CM3D2 Converter.anm_export

  1import re
  2import struct
  3import math
  4import unicodedata
  5import time
  6import bpy
  7import bmesh
  8import mathutils
  9from . import common
 10from . import compat
 11from .translations.pgettext_functions import *
 12from . import misc_DOPESHEET_MT_editor_menus
 13
 14
 15# メインオペレーター
 16@compat.BlRegister()
 17class CNV_OT_export_cm3d2_anm(bpy.types.Operator):
 18    bl_idname = 'export_anim.export_cm3d2_anm'
 19    bl_label = "CM3D2モーション (.anm)"
 20    bl_description = "カスタムメイド3D2のanmファイルを保存します"
 21    bl_options = {'REGISTER'}
 22
 23    filepath = bpy.props.StringProperty(subtype='FILE_PATH')
 24    filename_ext = ".anm"
 25    filter_glob = bpy.props.StringProperty(default="*.anm", options={'HIDDEN'})
 26
 27    scale = bpy.props.FloatProperty(name="倍率", default=0.2, min=0.1, max=100, soft_min=0.1, soft_max=100, step=100, precision=1, description="エクスポート時のメッシュ等の拡大率です")
 28    is_backup = bpy.props.BoolProperty(name="ファイルをバックアップ", default=True, description="ファイルに上書きする場合にバックアップファイルを複製します")
 29    version = bpy.props.IntProperty(name="ファイルバージョン", default=1000, min=1000, max=1111, soft_min=1000, soft_max=1111, step=1)
 30    
 31    #is_anm_data_text = bpy.props.BoolProperty(name="From Anm Text", default=False, description="Input data from JSON file")
 32    items = [
 33        ('ALL'  , "Bake All Frames"      , "Export every frame as a keyframe (legacy behavior, large file sizes)", 'SEQUENCE' , 1),
 34        ('KEYED', "Only Export Keyframes", "Only export keyframes and their tangents (for more advance users)"   , 'KEYINGSET', 2),
 35        ('TEXT' , "From Anm Text JSON"   , "Export data from the JSON in the 'AnmData' text file"                , 'TEXT'     , 3)
 36    ]
 37    export_method = bpy.props.EnumProperty(items=items, name="Export Method", default='ALL')
 38
 39    
 40    frame_start = bpy.props.IntProperty(name="開始フレーム", default=0, min=0, max=99999, soft_min=0, soft_max=99999, step=1)
 41    frame_end = bpy.props.IntProperty(name="最終フレーム", default=0, min=0, max=99999, soft_min=0, soft_max=99999, step=1)
 42    key_frame_count = bpy.props.IntProperty(name="キーフレーム数", default=1, min=1, max=99999, soft_min=1, soft_max=99999, step=1)
 43    time_scale = bpy.props.FloatProperty(name="再生速度", default=1.0, min=0.1, max=10.0, soft_min=0.1, soft_max=10.0, step=10, precision=1)
 44    is_keyframe_clean = bpy.props.BoolProperty(name="同じ変形のキーフレームを掃除", default=True)
 45    is_visual_transform = bpy.props.BoolProperty(name="Use Visual Transforms", default=True )
 46    is_smooth_handle = bpy.props.BoolProperty(name="キーフレーム間の変形をスムーズに", default=True)
 47
 48    items = [
 49        ('ARMATURE', "アーマチュア", "", 'OUTLINER_OB_ARMATURE', 1),
 50        ('ARMATURE_PROPERTY', "アーマチュア内プロパティ", "", 'ARMATURE_DATA', 2),
 51    ]
 52    bone_parent_from = bpy.props.EnumProperty(items=items, name="ボーン親情報の参照先", default='ARMATURE_PROPERTY')
 53
 54    is_remove_unkeyed_bone       = bpy.props.BoolProperty(name="Remove Unkeyed Bones", default=False)
 55    is_remove_alone_bone         = bpy.props.BoolProperty(name="親も子も存在しない", default=True)
 56    is_remove_ik_bone            = bpy.props.BoolProperty(name="名前がIK/Nubっぽい", default=True)
 57    is_remove_serial_number_bone = bpy.props.BoolProperty(name="名前が連番付き", default=True)
 58    is_remove_japanese_bone      = bpy.props.BoolProperty(name="名前に日本語が含まれる", default=True)
 59
 60    @classmethod
 61    def poll(cls, context):
 62        ob = context.active_object
 63        if ob and ob.type == 'ARMATURE':
 64            return True
 65        return False
 66
 67    def invoke(self, context, event):
 68        prefs = common.preferences()
 69
 70        ob = context.active_object
 71        arm = ob.data
 72        action_name = None
 73        if ob.animation_data and ob.animation_data.action:
 74            action_name = common.remove_serial_number(ob.animation_data.action.name)
 75
 76        if prefs.anm_default_path:
 77            self.filepath = common.default_cm3d2_dir(prefs.anm_default_path, action_name, "anm")
 78        else:
 79            self.filepath = common.default_cm3d2_dir(prefs.anm_export_path, action_name, "anm")
 80        self.frame_start = context.scene.frame_start
 81        self.frame_end = context.scene.frame_end
 82        self.scale = 1.0 / prefs.scale
 83        self.is_backup = bool(prefs.backup_ext)
 84        self.key_frame_count = (context.scene.frame_end - context.scene.frame_start) + 1
 85
 86        if "BoneData:0" in arm:
 87            self.bone_parent_from = 'ARMATURE_PROPERTY'
 88        else:
 89            self.bone_parent_from = 'ARMATURE'
 90
 91        context.window_manager.fileselect_add(self)
 92        return {'RUNNING_MODAL'}
 93
 94    def draw(self, context):
 95        self.layout.prop(self, 'scale')
 96
 97        box = self.layout.box()
 98        box.prop(self, 'is_backup', icon='FILE_BACKUP')
 99        box.prop(self, 'version')
100
101        #self.layout.prop(self, 'is_anm_data_text', icon='TEXT')
102        box = self.layout.box()
103        box.label(text="Export Method")
104        box.prop(self, 'export_method', expand=True)
105
106        box = self.layout.box()
107        box.enabled = not (self.export_method == 'TEXT')
108        box.prop(self, 'time_scale')
109        sub_box = box.box()
110        sub_box.enabled = (self.export_method == 'ALL')
111        row = sub_box.row()
112        row.prop(self, 'frame_start')
113        row.prop(self, 'frame_end')
114        sub_box.prop(self, 'key_frame_count')
115        sub_box.prop(self, 'is_keyframe_clean', icon='DISCLOSURE_TRI_DOWN')
116        sub_box.prop(self, 'is_smooth_handle', icon='SMOOTHCURVE')
117
118        sub_box = box.box()
119        sub_box.label(text="ボーン親情報の参照先", icon='FILE_PARENT')
120        sub_box.prop(self, 'bone_parent_from', icon='FILE_PARENT', expand=True)
121
122        sub_box = box.box()
123        sub_box.label(text="除外するボーン", icon='X')
124        column = sub_box.column(align=True)
125        column.prop(self, 'is_remove_unkeyed_bone'      , icon='KEY_DEHLT'              )
126        column.prop(self, 'is_remove_alone_bone'        , icon='UNLINKED'               )
127        column.prop(self, 'is_remove_ik_bone'           , icon='CONSTRAINT_BONE'        )
128        column.prop(self, 'is_remove_serial_number_bone', icon='SEQUENCE'               )
129        column.prop(self, 'is_remove_japanese_bone'     , icon=compat.icon('HOLDOUT_ON'))
130
131    def execute(self, context):
132        common.preferences().anm_export_path = self.filepath
133
134        try:
135            file = common.open_temporary(self.filepath, 'wb', is_backup=self.is_backup)
136        except:
137            self.report(type={'ERROR'}, message=f_tip_("ファイルを開くのに失敗しました、アクセス不可かファイルが存在しません。file={}", self.filepath))
138            return {'CANCELLED'}
139
140        try:
141            with file:
142                if self.export_method == 'TEXT':
143                    self.write_animation_from_text(context, file)
144                else:
145                    self.write_animation(context, file)
146        except common.CM3D2ExportException as e:
147            self.report(type={'ERROR'}, message=str(e))
148            return {'CANCELLED'}
149
150        return {'FINISHED'}
151
152    def get_animation_frames(self, context, fps, pose, bones, bone_parents):
153        anm_data_raw = {}
154        class KeyFrame:
155            def __init__(self, time, value, slope=None):
156                self.time = time
157                self.value = value
158                if slope:
159                    self.slope = slope
160                elif type(value) == mathutils.Vector:
161                    self.slope = mathutils.Vector.Fill(len(value))
162                elif type(value) == mathutils.Quaternion:
163                    self.slope = mathutils.Quaternion((0,0,0,0))
164                else:
165                    self.slope = 0
166
167        same_locs = {}
168        same_rots = {}
169        pre_rots = {}
170        for key_frame_index in range(self.key_frame_count):
171            if self.key_frame_count == 1:
172                frame = 0.0
173            else:
174                frame = (self.frame_end - self.frame_start) / (self.key_frame_count - 1) * key_frame_index + self.frame_start
175            context.scene.frame_set(frame=int(frame), subframe=frame - int(frame))
176            if compat.IS_LEGACY:
177                context.scene.update()
178            else:
179                layer = context.view_layer
180                layer.update()
181
182            time = frame / fps * (1.0 / self.time_scale)
183
184            for bone in bones:
185                if bone.name not in anm_data_raw:
186                    anm_data_raw[bone.name] = {"LOC": {}, "ROT": {}}
187                    same_locs[bone.name] = []
188                    same_rots[bone.name] = []
189
190                pose_bone = pose.bones[bone.name]
191                pose_mat = pose_bone.matrix.copy() #ob.convert_space(pose_bone=pose_bone, matrix=pose_bone.matrix, from_space='POSE', to_space='WORLD')
192                parent = bone_parents[bone.name]
193                if parent:
194                    pose_mat = compat.convert_bl_to_cm_bone_rotation(pose_mat)
195                    pose_mat = compat.mul(pose.bones[parent.name].matrix.inverted(), pose_mat)
196                    pose_mat = compat.convert_bl_to_cm_bone_space(pose_mat)
197                else:
198                    pose_mat = compat.convert_bl_to_cm_bone_rotation(pose_mat)
199                    pose_mat = compat.convert_bl_to_cm_space(pose_mat)
200
201                loc = pose_mat.to_translation() * self.scale
202                rot = pose_mat.to_quaternion()
203
204                # This fixes rotations that jump to alternate representations.
205                if bone.name in pre_rots:
206                    if 5.0 < pre_rots[bone.name].rotation_difference(rot).angle:
207                        rot.w, rot.x, rot.y, rot.z = -rot.w, -rot.x, -rot.y, -rot.z
208                pre_rots[bone.name] = rot.copy()
209
210                #if parent:
211                #    #loc.x, loc.y, loc.z = -loc.y, -loc.x, loc.z
212                #    loc = compat.convert_bl_to_cm_bone_space(loc)
213                #    
214                #    # quat.w, quat.x, quat.y, quat.z = quat.w, -quat.z, quat.x, -quat.y
215                #    #rot.w, rot.x, rot.y, rot.z = rot.w, rot.y, rot.x, -rot.z
216                #    rot.w, rot.x, rot.y, rot.z = rot.w, rot.y, -rot.z, -rot.x
217                #
218                #else:
219                #    loc.x, loc.y, loc.z = -loc.x, loc.z, -loc.y
220                #
221                #    fix_quat = mathutils.Euler((0, 0, math.radians(-90)), 'XYZ').to_quaternion()
222                #    fix_quat2 = mathutils.Euler((math.radians(-90), 0, 0), 'XYZ').to_quaternion()
223                #    rot = compat.mul3(rot, fix_quat, fix_quat2)
224                #
225                #    rot.w, rot.x, rot.y, rot.z = -rot.y, -rot.z, -rot.x, rot.w
226                
227                if not self.is_keyframe_clean or key_frame_index == 0 or key_frame_index == self.key_frame_count - 1:
228                    anm_data_raw[bone.name]["LOC"][time] = loc.copy()
229                    anm_data_raw[bone.name]["ROT"][time] = rot.copy()
230
231                    if self.is_keyframe_clean:
232                        same_locs[bone.name].append(KeyFrame(time, loc.copy()))
233                        same_rots[bone.name].append(KeyFrame(time, rot.copy()))
234                else:
235                    def is_mismatch(a, b):
236                        return 0.000001 < abs(a - b)
237
238                    a = same_locs[bone.name][-1].value - loc
239                    b = same_locs[bone.name][-1].slope
240                    if is_mismatch(a.x, b.x) or is_mismatch(a.y, b.y) or is_mismatch(a.z, b.z):
241                        if 2 <= len(same_locs[bone.name]):
242                            anm_data_raw[bone.name]["LOC"][same_locs[bone.name][-1].time] = same_locs[bone.name][-1].value.copy()
243                        anm_data_raw[bone.name]["LOC"][time] = loc.copy()
244                        same_locs[bone.name] = [KeyFrame(time, loc.copy(), a.copy())] # update last position and slope
245                    else:
246                        same_locs[bone.name].append(KeyFrame(time, loc.copy(), b.copy())) # update last position, but not last slope
247                    
248                    a = same_rots[bone.name][-1].value - rot
249                    b = same_rots[bone.name][-1].slope
250                    if is_mismatch(a.w, b.w) or is_mismatch(a.x, b.x) or is_mismatch(a.y, b.y) or is_mismatch(a.z, b.z):
251                        if 2 <= len(same_rots[bone.name]):
252                            anm_data_raw[bone.name]["ROT"][same_rots[bone.name][-1].time] = same_rots[bone.name][-1].value.copy()
253                        anm_data_raw[bone.name]["ROT"][time] = rot.copy()
254                        same_rots[bone.name] = [KeyFrame(time, rot.copy(), a.copy())] # update last position and slope
255                    else:
256                        same_rots[bone.name].append(KeyFrame(time, rot.copy(), b.copy())) # update last position, but not last slope
257        
258        return anm_data_raw
259
260    def get_animation_keyframes(self, context, fps, pose, keyed_bones, fcurves):
261        anm_data_raw = {}
262
263        prop_sizes = {'location': 3, 'rotation_quaternion': 4, 'rotation_euler': 3}
264        
265        #class KeyFrame:
266        #    def __init__(self, time, value):
267        #        self.time = time
268        #        self.value = value
269        #same_locs = {}
270        #same_rots = {}
271        #pre_rots = {}
272        
273        def _convert_loc(pose_bone, loc):
274            loc = mathutils.Vector(loc)
275            loc = compat.mul(pose_bone.bone.matrix_local, loc)
276            if pose_bone.parent:
277                loc = compat.mul(pose_bone.parent.bone.matrix_local.inverted(), loc)
278                loc = compat.convert_bl_to_cm_bone_space(loc)
279            else:
280                loc = compat.convert_bl_to_cm_space(loc)
281            return loc * self.scale
282        """
283        def _convert_quat(pose_bone, quat):
284            #quat = mathutils.Quaternion(quat)
285            #'''Can't use matrix transforms here as they would mess up interpolation.'''
286            #quat = compat.mul(pose_bone.bone.matrix_local.to_quaternion(), quat)
287            
288            quat_mat = mathutils.Quaternion(quat).to_matrix().to_4x4()
289            quat_mat = compat.mul(pose_bone.bone.matrix_local, quat_mat)
290            #quat = quat_mat.to_quaternion()
291            if pose_bone.parent:
292                ## inverse of quat.w, quat.x, quat.y, quat.z = quat.w, -quat.z, quat.x, -quat.y
293                #quat.w, quat.x, quat.y, quat.z = quat.w, quat.y, -quat.z, -quat.x
294                #quat = compat.mul(pose_bone.parent.bone.matrix_local.to_quaternion().inverted(), quat)
295                ##quat = compat.mul(pose_bone.parent.bone.matrix_local.inverted().to_quaternion(), quat)\
296                quat_mat = compat.convert_bl_to_cm_bone_rotation(quat_mat)
297                quat_mat = compat.mul(pose_bone.parent.bone.matrix_local.inverted(), quat_mat)
298                quat_mat = compat.convert_bl_to_cm_bone_space(quat_mat)
299                quat = quat_mat.to_quaternion()
300            else:
301                #fix_quat = mathutils.Euler((0, 0, math.radians(-90)), 'XYZ').to_quaternion()
302                #fix_quat2 = mathutils.Euler((math.radians(-90), 0, 0), 'XYZ').to_quaternion()
303                #quat = compat.mul3(quat, fix_quat, fix_quat2)
304                #
305                #quat.w, quat.x, quat.y, quat.z = -quat.y, -quat.z, -quat.x, quat.w
306                
307                #quat.w, quat.x, quat.y, quat.z = quat.w, quat.y, -quat.z, -quat.x
308                #quat = compat.mul(mathutils.Matrix.Rotation(math.radians(90.0), 4, 'Z').to_quaternion(), quat)
309
310                quat_mat = compat.convert_bl_to_cm_bone_rotation(quat_mat)
311                quat_mat = compat.convert_bl_to_cm_space(quat_mat)
312                quat = quat_mat.to_quaternion()
313            return quat
314        """
315
316        def _convert_quat(pose_bone, quat):
317            bone_quat = pose_bone.bone.matrix.to_quaternion()
318            quat = mathutils.Quaternion(quat)
319
320            '''Can't use matrix transforms here as they would mess up interpolation.'''
321            quat = compat.mul(bone_quat, quat)
322            
323            if pose_bone.bone.parent:
324                #quat.w, quat.x, quat.y, quat.z = quat.w, -quat.z, quat.x, -quat.y
325                quat.w, quat.y, quat.x, quat.z = quat.w, -quat.z, quat.y, -quat.x
326            else:
327                quat = compat.mul(mathutils.Matrix.Rotation(math.radians(90.0), 4, 'Z').to_quaternion(), quat)
328                quat.w, quat.y, quat.x, quat.z = quat.w, -quat.z, quat.y, -quat.x
329            return quat
330
331        for prop, prop_keyed_bones in keyed_bones.items():
332            #self.report(type={'INFO'}, message=f_tip_("{prop} {list}", prop=prop, list=prop_keyed_bones))
333            for bone_name in prop_keyed_bones:
334                if bone_name not in anm_data_raw:
335                    anm_data_raw[bone_name] = {}
336                    #same_locs[bone_name] = []
337                    #same_rots[bone_name] = []
338                
339                pose_bone = pose.bones[bone_name]
340                rna_data_path = 'pose.bones["{bone_name}"].{property}'.format(bone_name=bone_name, property=prop)
341                prop_fcurves = [ fcurves.find(rna_data_path, index=axis_index) for axis_index in range(prop_sizes[prop]) ]
342                
343                # Create missing fcurves, and make existing fcurves CM3D2 compatible.
344                for axis_index, fcurve in enumerate(prop_fcurves):
345                    if not fcurve:
346                        fcurve = fcurves.new(rna_data_path, index=axis_index, action_group=pose_bone.name)
347                        prop_fcurves[axis_index] = fcurve
348                        self.report(type={'WARNING'}, message=f_tip_("Creating missing FCurve for {path}[{index}]", path=rna_data_path, index=axis_index))
349                    else:
350                        override = context.copy()
351                        override['active_editable_fcurve'] = fcurve
352                        bpy.ops.fcurve.convert_to_cm3d2_interpolation(override, only_selected=False, keep_reports=True)
353                        for kwargs in misc_DOPESHEET_MT_editor_menus.REPORTS:
354                            self.report(**kwargs)
355                        misc_DOPESHEET_MT_editor_menus.REPORTS.clear()
356
357
358                # Create a list by frame, indicating wether or not there is a keyframe at that time for each fcurve
359                is_keyframes = {}
360                for fcurve in prop_fcurves:
361                    for keyframe in fcurve.keyframe_points:
362                        frame = keyframe.co[0]
363                        if frame not in is_keyframes:
364                            is_keyframes[frame] = [False] * prop_sizes[prop]
365                        is_keyframes[frame][fcurve.array_index] = True
366                
367                # Make sure that no keyframe times are missing any components
368                for frame, is_axes in is_keyframes.items():
369                    for axis_index, is_axis in enumerate(is_axes):
370                        if not is_axis:
371                            fcurve = prop_fcurves[axis_index]
372                            keyframe = fcurve.keyframe_points.insert(
373                                frame         = frame                 , 
374                                value         = fcurve.evaluate(frame), 
375                                options       = {'NEEDED', 'FAST'}                        
376                            )
377                            self.report(type={'WARNING'}, message=f_tip_("Creating missing keyframe @ frame {frame} for {path}[{index}]", path=rna_data_path, index=axis_index, frame=frame))
378                
379                for fcurve in prop_fcurves:
380                    fcurve.update()
381                
382                for keyframe_index, frame in enumerate(is_keyframes.keys()):
383                    time = frame / fps * (1.0 / self.time_scale)
384
385                    _kf = lambda fcurve: fcurve.keyframe_points[keyframe_index]
386                    raw_keyframe = [ _kf(fc).co[1] for fc in prop_fcurves ]                                                                            
387                    tangent_in   = [ ( _kf(fc).handle_left [1] - _kf(fc).co[1] ) / ( _kf(fc).handle_left [0] - _kf(fc).co[0] ) * fps for fc in prop_fcurves ]
388                    tangent_out  = [ ( _kf(fc).handle_right[1] - _kf(fc).co[1] ) / ( _kf(fc).handle_right[0] - _kf(fc).co[0] ) * fps for fc in prop_fcurves ]
389                                                   
390                    if prop == 'location':
391                        if 'LOC' not in anm_data_raw[bone_name]:
392                            anm_data_raw[bone_name]['LOC'    ] = {}
393                            anm_data_raw[bone_name]['LOC_IN' ] = {}
394                            anm_data_raw[bone_name]['LOC_OUT'] = {}
395                        anm_data_raw[bone_name]['LOC'    ][time] = _convert_loc(pose_bone, raw_keyframe).copy()
396                        anm_data_raw[bone_name]['LOC_IN' ][time] = _convert_loc(pose_bone, tangent_in  ).copy()
397                        anm_data_raw[bone_name]['LOC_OUT'][time] = _convert_loc(pose_bone, tangent_out ).copy()
398                    elif prop == 'rotation_quaternion':
399                        if 'ROT' not in anm_data_raw[bone_name]:
400                            anm_data_raw[bone_name]['ROT'    ] = {}
401                            anm_data_raw[bone_name]['ROT_IN' ] = {}
402                            anm_data_raw[bone_name]['ROT_OUT'] = {}
403                        anm_data_raw[bone_name]['ROT'    ][time] = _convert_quat(pose_bone, raw_keyframe).copy()
404                        anm_data_raw[bone_name]['ROT_OUT'][time] = _convert_quat(pose_bone, tangent_out ).copy()
405                        anm_data_raw[bone_name]['ROT_IN' ][time] = _convert_quat(pose_bone, tangent_in  ).copy()
406                        # - - - Alternative Method - - -
407                        #raw_keyframe = mathutils.Quaternion(raw_keyframe)
408                        #tangent_in   = mathutils.Quaternion(tangent_in)
409                        #tangent_out  = mathutils.Quaternion(tangent_out)
410                        #converted_quat = _convert_quat(pose_bone, raw_keyframe).copy()
411                        #anm_data_raw[bone_name]['ROT'    ][time] = converted_quat.copy()
412                        #anm_data_raw[bone_name]['ROT_IN' ][time] = converted_quat.inverted() @ _convert_quat(pose_bone, raw_keyframe @ tangent_in  )
413                        #anm_data_raw[bone_name]['ROT_OUT'][time] = converted_quat.inverted() @ _convert_quat(pose_bone, raw_keyframe @ tangent_out )
414        
415        return anm_data_raw
416
417    def write_animation(self, context, file):
418        ob = context.active_object
419        arm = ob.data
420        pose = ob.pose
421        fps = context.scene.render.fps
422
423
424        bone_parents = {}
425        if self.bone_parent_from == 'ARMATURE_PROPERTY':
426            for i in range(9999):
427                name = "BoneData:" + str(i)
428                if name not in arm:
429                    continue
430                elems = arm[name].split(",")
431                if len(elems) != 5:
432                    continue
433                if elems[0] in arm.bones:
434                    if elems[2] in arm.bones:
435                        bone_parents[elems[0]] = arm.bones[elems[2]]
436                    else:
437                        bone_parents[elems[0]] = None
438            for bone in arm.bones:
439                if bone.name in bone_parents:
440                    continue
441                bone_parents[bone.name] = bone.parent
442        else:
443            for bone in arm.bones:
444                bone_parents[bone.name] = bone.parent
445
446        copied_action = None
447        if ob.animation_data and ob.animation_data.action:
448            copied_action = ob.animation_data.action.copy()
449            copied_action.name = ob.animation_data.action.name + "__anm_export"
450            fcurves = copied_action.fcurves
451            keyed_bones = {'location': [], 'rotation_quaternion': [], 'rotation_euler': []}
452            for bone in arm.bones:
453                rna_data_stub = 'pose.bones["{bone_name}"]'.format(bone_name=bone.name)
454                for prop, axes in [('location', 3), ('rotation_quaternion', 4), ('rotation_euler', 3)]:
455                    found_fcurve = False
456                    for axis_index in range(0, axes):
457                        if fcurves.find(rna_data_stub + '.' + prop, index=axis_index):
458                            found_fcurve = True
459                            break
460                    if found_fcurve:
461                        keyed_bones[prop].append(bone.name)
462
463        elif self.export_method == 'KEYED' or self.is_remove_unkeyed_bone:
464            raise common.CM3D2ExportException(
465                "Active armature has no animation data / action. Please use \"{method}\" with \"{option}\" disabled, or bake keyframes before exporting.".format(
466                    method = "Bake All Frames",
467                    option = "Remove Unkeyed Bones"
468                )
469            )
470
471
472
473
474        def is_japanese(string):
475            for ch in string:
476                name = unicodedata.name(ch)
477                if 'CJK UNIFIED' in name or 'HIRAGANA' in name or 'KATAKANA' in name:
478                    return True
479            return False
480        bones = []
481        already_bone_names = []
482        bones_queue = arm.bones[:]
483        while len(bones_queue):
484            bone = bones_queue.pop(0)
485
486            if not bone_parents[bone.name]:
487                already_bone_names.append(bone.name)
488                if self.is_remove_serial_number_bone:
489                    if common.has_serial_number(bone.name):
490                        continue
491                if self.is_remove_japanese_bone:
492                    if is_japanese(bone.name):
493                        continue
494                if self.is_remove_alone_bone and len(bone.children) == 0:
495                    continue
496                if self.is_remove_unkeyed_bone:
497                    is_keyed = False
498                    for prop in keyed_bones:
499                        if bone.name in keyed_bones[prop]:
500                            is_keyed = True
501                            break
502                    if not is_keyed:
503                        continue
504                bones.append(bone)
505                continue
506            elif bone_parents[bone.name].name in already_bone_names:
507                already_bone_names.append(bone.name)
508                if self.is_remove_serial_number_bone:
509                    if common.has_serial_number(bone.name):
510                        continue
511                if self.is_remove_japanese_bone:
512                    if is_japanese(bone.name):
513                        continue
514                if self.is_remove_ik_bone:
515                    bone_name_low = bone.name.lower()
516                    if '_ik_' in bone_name_low or bone_name_low.endswith('_nub') or bone.name.endswith('Nub'):
517                        continue
518                if self.is_remove_unkeyed_bone:
519                    is_keyed = False
520                    for prop in keyed_bones:
521                        if bone.name in keyed_bones[prop]:
522                            is_keyed = True
523                            break
524                    if not is_keyed:
525                        continue
526                bones.append(bone)
527                continue
528
529            bones_queue.append(bone)
530
531        if self.export_method == 'ALL':
532            anm_data_raw = self.get_animation_frames(context, fps, pose, bones, bone_parents)
533        elif self.export_method == 'KEYED':
534            anm_data_raw = self.get_animation_keyframes(context, fps, pose, keyed_bones, fcurves)
535
536        if copied_action:
537            context.blend_data.actions.remove(copied_action, do_unlink=True, do_id_user=True, do_ui_user=True)
538
539        anm_data = {}
540        for bone_name, channels in anm_data_raw.items():
541            anm_data[bone_name] = {100: {}, 101: {}, 102: {}, 103: {}, 104: {}, 105: {}, 106: {}}
542            if channels.get('LOC'):
543                has_tangents = bool(channels.get('LOC_IN') and channels.get('LOC_OUT'))
544                for time, loc in channels["LOC"].items():
545                    tangent_in  = channels['LOC_IN' ][time] if has_tangents else mathutils.Vector()
546                    tangent_out = channels['LOC_OUT'][time] if has_tangents else mathutils.Vector()
547                    anm_data[bone_name][104][time] = (loc.x, tangent_in.x, tangent_out.x)
548                    anm_data[bone_name][105][time] = (loc.y, tangent_in.y, tangent_out.y)
549                    anm_data[bone_name][106][time] = (loc.z, tangent_in.z, tangent_out.z)
550            if channels.get('ROT'):
551                has_tangents = bool(channels.get('ROT_IN') and channels.get('ROT_OUT'))
552                for time, rot in channels["ROT"].items():
553                    tangent_in  = channels['ROT_IN' ][time] if has_tangents else mathutils.Quaternion((0,0,0,0))
554                    tangent_out = channels['ROT_OUT'][time] if has_tangents else mathutils.Quaternion((0,0,0,0))
555                    anm_data[bone_name][100][time] = (rot.x, tangent_in.x, tangent_out.x)
556                    anm_data[bone_name][101][time] = (rot.y, tangent_in.y, tangent_out.y)
557                    anm_data[bone_name][102][time] = (rot.z, tangent_in.z, tangent_out.z)
558                    anm_data[bone_name][103][time] = (rot.w, tangent_in.w, tangent_out.w)
559                                                      
560        time_step = 1 / fps * (1.0 / self.time_scale)
561
562
563        ''' Write data to the file '''
564
565        common.write_str(file, 'CM3D2_ANIM')
566        file.write(struct.pack('<i', self.version))
567
568        for bone in bones:
569            if not anm_data.get(bone.name):
570                continue
571
572            file.write(struct.pack('<?', True))
573
574            bone_names = [bone.name]
575            current_bone = bone
576            while bone_parents[current_bone.name]:
577                bone_names.append(bone_parents[current_bone.name].name)
578                current_bone = bone_parents[current_bone.name]
579
580            bone_names.reverse()
581            common.write_str(file, "/".join(bone_names))
582            
583            for channel_id, keyframes in sorted(anm_data[bone.name].items(), key=lambda x: x[0]):
584                file.write(struct.pack('<B', channel_id))
585                file.write(struct.pack('<i', len(keyframes)))
586
587                keyframes_list = sorted(keyframes.items(), key=lambda x: x[0])
588                for i in range(len(keyframes_list)):
589                    x = keyframes_list[i][0]
590                    y, dydx_in, dydx_out = keyframes_list[i][1]
591
592                    if len(keyframes_list) <= 1:
593                        file.write(struct.pack('<f', x))
594                        file.write(struct.pack('<f', y))
595                        file.write(struct.pack('<2f', 0.0, 0.0))
596                        continue
597
598                    file.write(struct.pack('<f', x))
599                    file.write(struct.pack('<f', y))
600
601                    if self.is_smooth_handle and self.export_method == 'ALL':
602                        if i == 0:
603                            prev_x = x - (keyframes_list[i + 1][0] - x)
604                            prev_y = y - (keyframes_list[i + 1][1][0] - y)
605                            next_x = keyframes_list[i + 1][0]
606                            next_y = keyframes_list[i + 1][1][0]
607                        elif i == len(keyframes_list) - 1:
608                            prev_x = keyframes_list[i - 1][0]
609                            prev_y = keyframes_list[i - 1][1][0]
610                            next_x = x + (x - keyframes_list[i - 1][0])
611                            next_y = y + (y - keyframes_list[i - 1][1][0])
612                        else:
613                            prev_x = keyframes_list[i - 1][0]
614                            prev_y = keyframes_list[i - 1][1][0]
615                            next_x = keyframes_list[i + 1][0]
616                            next_y = keyframes_list[i + 1][1][0]
617
618                        prev_rad = (prev_y - y) / (prev_x - x)
619                        next_rad = (next_y - y) / (next_x - x)
620                        join_rad = (prev_rad + next_rad) / 2
621
622                        tan_in  = join_rad if x - prev_x <= time_step * 1.5 else prev_rad
623                        tan_out = join_rad if next_x - x <= time_step * 1.5 else next_rad
624                        
625                        file.write(struct.pack('<2f', tan_in, tan_out))
626                        #file.write(struct.pack('<2f', join_rad, join_rad))
627                        #file.write(struct.pack('<2f', prev_rad, next_rad))
628                    else:
629                        file.write(struct.pack('<2f', dydx_in, dydx_out))
630
631        file.write(struct.pack('<?', False))
632
633    def write_animation_from_text(self, context, file):
634        txt = context.blend_data.texts.get("AnmData")
635        if not txt:
636            raise common.CM3D2ExportException("There is no 'AnmData' text file.")
637
638        import json
639        anm_data = json.loads(txt.as_string())
640
641        common.write_str(file, 'CM3D2_ANIM')
642        file.write(struct.pack('<i', self.version))
643
644        for base_bone_name, bone_data in anm_data.items():
645            path = bone_data['path']
646            file.write(struct.pack('<?', True))
647            common.write_str(file, path)
648
649            for channel_id, channel in bone_data['channels'].items():
650                file.write(struct.pack('<B', int(channel_id)))
651                channel_data_count = len(channel)
652                file.write(struct.pack('<i', channel_data_count))
653                for channel_data in channel:
654                    frame = channel_data['frame']
655                    data = ( channel_data['f0'], channel_data['f1'], channel_data['f2'] )
656                    file.write(struct.pack('<f' , frame))
657                    file.write(struct.pack('<3f', *data ))
658
659        file.write(struct.pack('<?', False))
660
661
662
663
664
665# メニューに登録する関数
666def menu_func(self, context):
667    self.layout.operator(CNV_OT_export_cm3d2_anm.bl_idname, icon_value=common.kiss_icon())
@compat.BlRegister()
class CNV_OT_export_cm3d2_anm(bpy_types.Operator):
 17@compat.BlRegister()
 18class CNV_OT_export_cm3d2_anm(bpy.types.Operator):
 19    bl_idname = 'export_anim.export_cm3d2_anm'
 20    bl_label = "CM3D2モーション (.anm)"
 21    bl_description = "カスタムメイド3D2のanmファイルを保存します"
 22    bl_options = {'REGISTER'}
 23
 24    filepath = bpy.props.StringProperty(subtype='FILE_PATH')
 25    filename_ext = ".anm"
 26    filter_glob = bpy.props.StringProperty(default="*.anm", options={'HIDDEN'})
 27
 28    scale = bpy.props.FloatProperty(name="倍率", default=0.2, min=0.1, max=100, soft_min=0.1, soft_max=100, step=100, precision=1, description="エクスポート時のメッシュ等の拡大率です")
 29    is_backup = bpy.props.BoolProperty(name="ファイルをバックアップ", default=True, description="ファイルに上書きする場合にバックアップファイルを複製します")
 30    version = bpy.props.IntProperty(name="ファイルバージョン", default=1000, min=1000, max=1111, soft_min=1000, soft_max=1111, step=1)
 31    
 32    #is_anm_data_text = bpy.props.BoolProperty(name="From Anm Text", default=False, description="Input data from JSON file")
 33    items = [
 34        ('ALL'  , "Bake All Frames"      , "Export every frame as a keyframe (legacy behavior, large file sizes)", 'SEQUENCE' , 1),
 35        ('KEYED', "Only Export Keyframes", "Only export keyframes and their tangents (for more advance users)"   , 'KEYINGSET', 2),
 36        ('TEXT' , "From Anm Text JSON"   , "Export data from the JSON in the 'AnmData' text file"                , 'TEXT'     , 3)
 37    ]
 38    export_method = bpy.props.EnumProperty(items=items, name="Export Method", default='ALL')
 39
 40    
 41    frame_start = bpy.props.IntProperty(name="開始フレーム", default=0, min=0, max=99999, soft_min=0, soft_max=99999, step=1)
 42    frame_end = bpy.props.IntProperty(name="最終フレーム", default=0, min=0, max=99999, soft_min=0, soft_max=99999, step=1)
 43    key_frame_count = bpy.props.IntProperty(name="キーフレーム数", default=1, min=1, max=99999, soft_min=1, soft_max=99999, step=1)
 44    time_scale = bpy.props.FloatProperty(name="再生速度", default=1.0, min=0.1, max=10.0, soft_min=0.1, soft_max=10.0, step=10, precision=1)
 45    is_keyframe_clean = bpy.props.BoolProperty(name="同じ変形のキーフレームを掃除", default=True)
 46    is_visual_transform = bpy.props.BoolProperty(name="Use Visual Transforms", default=True )
 47    is_smooth_handle = bpy.props.BoolProperty(name="キーフレーム間の変形をスムーズに", default=True)
 48
 49    items = [
 50        ('ARMATURE', "アーマチュア", "", 'OUTLINER_OB_ARMATURE', 1),
 51        ('ARMATURE_PROPERTY', "アーマチュア内プロパティ", "", 'ARMATURE_DATA', 2),
 52    ]
 53    bone_parent_from = bpy.props.EnumProperty(items=items, name="ボーン親情報の参照先", default='ARMATURE_PROPERTY')
 54
 55    is_remove_unkeyed_bone       = bpy.props.BoolProperty(name="Remove Unkeyed Bones", default=False)
 56    is_remove_alone_bone         = bpy.props.BoolProperty(name="親も子も存在しない", default=True)
 57    is_remove_ik_bone            = bpy.props.BoolProperty(name="名前がIK/Nubっぽい", default=True)
 58    is_remove_serial_number_bone = bpy.props.BoolProperty(name="名前が連番付き", default=True)
 59    is_remove_japanese_bone      = bpy.props.BoolProperty(name="名前に日本語が含まれる", default=True)
 60
 61    @classmethod
 62    def poll(cls, context):
 63        ob = context.active_object
 64        if ob and ob.type == 'ARMATURE':
 65            return True
 66        return False
 67
 68    def invoke(self, context, event):
 69        prefs = common.preferences()
 70
 71        ob = context.active_object
 72        arm = ob.data
 73        action_name = None
 74        if ob.animation_data and ob.animation_data.action:
 75            action_name = common.remove_serial_number(ob.animation_data.action.name)
 76
 77        if prefs.anm_default_path:
 78            self.filepath = common.default_cm3d2_dir(prefs.anm_default_path, action_name, "anm")
 79        else:
 80            self.filepath = common.default_cm3d2_dir(prefs.anm_export_path, action_name, "anm")
 81        self.frame_start = context.scene.frame_start
 82        self.frame_end = context.scene.frame_end
 83        self.scale = 1.0 / prefs.scale
 84        self.is_backup = bool(prefs.backup_ext)
 85        self.key_frame_count = (context.scene.frame_end - context.scene.frame_start) + 1
 86
 87        if "BoneData:0" in arm:
 88            self.bone_parent_from = 'ARMATURE_PROPERTY'
 89        else:
 90            self.bone_parent_from = 'ARMATURE'
 91
 92        context.window_manager.fileselect_add(self)
 93        return {'RUNNING_MODAL'}
 94
 95    def draw(self, context):
 96        self.layout.prop(self, 'scale')
 97
 98        box = self.layout.box()
 99        box.prop(self, 'is_backup', icon='FILE_BACKUP')
100        box.prop(self, 'version')
101
102        #self.layout.prop(self, 'is_anm_data_text', icon='TEXT')
103        box = self.layout.box()
104        box.label(text="Export Method")
105        box.prop(self, 'export_method', expand=True)
106
107        box = self.layout.box()
108        box.enabled = not (self.export_method == 'TEXT')
109        box.prop(self, 'time_scale')
110        sub_box = box.box()
111        sub_box.enabled = (self.export_method == 'ALL')
112        row = sub_box.row()
113        row.prop(self, 'frame_start')
114        row.prop(self, 'frame_end')
115        sub_box.prop(self, 'key_frame_count')
116        sub_box.prop(self, 'is_keyframe_clean', icon='DISCLOSURE_TRI_DOWN')
117        sub_box.prop(self, 'is_smooth_handle', icon='SMOOTHCURVE')
118
119        sub_box = box.box()
120        sub_box.label(text="ボーン親情報の参照先", icon='FILE_PARENT')
121        sub_box.prop(self, 'bone_parent_from', icon='FILE_PARENT', expand=True)
122
123        sub_box = box.box()
124        sub_box.label(text="除外するボーン", icon='X')
125        column = sub_box.column(align=True)
126        column.prop(self, 'is_remove_unkeyed_bone'      , icon='KEY_DEHLT'              )
127        column.prop(self, 'is_remove_alone_bone'        , icon='UNLINKED'               )
128        column.prop(self, 'is_remove_ik_bone'           , icon='CONSTRAINT_BONE'        )
129        column.prop(self, 'is_remove_serial_number_bone', icon='SEQUENCE'               )
130        column.prop(self, 'is_remove_japanese_bone'     , icon=compat.icon('HOLDOUT_ON'))
131
132    def execute(self, context):
133        common.preferences().anm_export_path = self.filepath
134
135        try:
136            file = common.open_temporary(self.filepath, 'wb', is_backup=self.is_backup)
137        except:
138            self.report(type={'ERROR'}, message=f_tip_("ファイルを開くのに失敗しました、アクセス不可かファイルが存在しません。file={}", self.filepath))
139            return {'CANCELLED'}
140
141        try:
142            with file:
143                if self.export_method == 'TEXT':
144                    self.write_animation_from_text(context, file)
145                else:
146                    self.write_animation(context, file)
147        except common.CM3D2ExportException as e:
148            self.report(type={'ERROR'}, message=str(e))
149            return {'CANCELLED'}
150
151        return {'FINISHED'}
152
153    def get_animation_frames(self, context, fps, pose, bones, bone_parents):
154        anm_data_raw = {}
155        class KeyFrame:
156            def __init__(self, time, value, slope=None):
157                self.time = time
158                self.value = value
159                if slope:
160                    self.slope = slope
161                elif type(value) == mathutils.Vector:
162                    self.slope = mathutils.Vector.Fill(len(value))
163                elif type(value) == mathutils.Quaternion:
164                    self.slope = mathutils.Quaternion((0,0,0,0))
165                else:
166                    self.slope = 0
167
168        same_locs = {}
169        same_rots = {}
170        pre_rots = {}
171        for key_frame_index in range(self.key_frame_count):
172            if self.key_frame_count == 1:
173                frame = 0.0
174            else:
175                frame = (self.frame_end - self.frame_start) / (self.key_frame_count - 1) * key_frame_index + self.frame_start
176            context.scene.frame_set(frame=int(frame), subframe=frame - int(frame))
177            if compat.IS_LEGACY:
178                context.scene.update()
179            else:
180                layer = context.view_layer
181                layer.update()
182
183            time = frame / fps * (1.0 / self.time_scale)
184
185            for bone in bones:
186                if bone.name not in anm_data_raw:
187                    anm_data_raw[bone.name] = {"LOC": {}, "ROT": {}}
188                    same_locs[bone.name] = []
189                    same_rots[bone.name] = []
190
191                pose_bone = pose.bones[bone.name]
192                pose_mat = pose_bone.matrix.copy() #ob.convert_space(pose_bone=pose_bone, matrix=pose_bone.matrix, from_space='POSE', to_space='WORLD')
193                parent = bone_parents[bone.name]
194                if parent:
195                    pose_mat = compat.convert_bl_to_cm_bone_rotation(pose_mat)
196                    pose_mat = compat.mul(pose.bones[parent.name].matrix.inverted(), pose_mat)
197                    pose_mat = compat.convert_bl_to_cm_bone_space(pose_mat)
198                else:
199                    pose_mat = compat.convert_bl_to_cm_bone_rotation(pose_mat)
200                    pose_mat = compat.convert_bl_to_cm_space(pose_mat)
201
202                loc = pose_mat.to_translation() * self.scale
203                rot = pose_mat.to_quaternion()
204
205                # This fixes rotations that jump to alternate representations.
206                if bone.name in pre_rots:
207                    if 5.0 < pre_rots[bone.name].rotation_difference(rot).angle:
208                        rot.w, rot.x, rot.y, rot.z = -rot.w, -rot.x, -rot.y, -rot.z
209                pre_rots[bone.name] = rot.copy()
210
211                #if parent:
212                #    #loc.x, loc.y, loc.z = -loc.y, -loc.x, loc.z
213                #    loc = compat.convert_bl_to_cm_bone_space(loc)
214                #    
215                #    # quat.w, quat.x, quat.y, quat.z = quat.w, -quat.z, quat.x, -quat.y
216                #    #rot.w, rot.x, rot.y, rot.z = rot.w, rot.y, rot.x, -rot.z
217                #    rot.w, rot.x, rot.y, rot.z = rot.w, rot.y, -rot.z, -rot.x
218                #
219                #else:
220                #    loc.x, loc.y, loc.z = -loc.x, loc.z, -loc.y
221                #
222                #    fix_quat = mathutils.Euler((0, 0, math.radians(-90)), 'XYZ').to_quaternion()
223                #    fix_quat2 = mathutils.Euler((math.radians(-90), 0, 0), 'XYZ').to_quaternion()
224                #    rot = compat.mul3(rot, fix_quat, fix_quat2)
225                #
226                #    rot.w, rot.x, rot.y, rot.z = -rot.y, -rot.z, -rot.x, rot.w
227                
228                if not self.is_keyframe_clean or key_frame_index == 0 or key_frame_index == self.key_frame_count - 1:
229                    anm_data_raw[bone.name]["LOC"][time] = loc.copy()
230                    anm_data_raw[bone.name]["ROT"][time] = rot.copy()
231
232                    if self.is_keyframe_clean:
233                        same_locs[bone.name].append(KeyFrame(time, loc.copy()))
234                        same_rots[bone.name].append(KeyFrame(time, rot.copy()))
235                else:
236                    def is_mismatch(a, b):
237                        return 0.000001 < abs(a - b)
238
239                    a = same_locs[bone.name][-1].value - loc
240                    b = same_locs[bone.name][-1].slope
241                    if is_mismatch(a.x, b.x) or is_mismatch(a.y, b.y) or is_mismatch(a.z, b.z):
242                        if 2 <= len(same_locs[bone.name]):
243                            anm_data_raw[bone.name]["LOC"][same_locs[bone.name][-1].time] = same_locs[bone.name][-1].value.copy()
244                        anm_data_raw[bone.name]["LOC"][time] = loc.copy()
245                        same_locs[bone.name] = [KeyFrame(time, loc.copy(), a.copy())] # update last position and slope
246                    else:
247                        same_locs[bone.name].append(KeyFrame(time, loc.copy(), b.copy())) # update last position, but not last slope
248                    
249                    a = same_rots[bone.name][-1].value - rot
250                    b = same_rots[bone.name][-1].slope
251                    if is_mismatch(a.w, b.w) or is_mismatch(a.x, b.x) or is_mismatch(a.y, b.y) or is_mismatch(a.z, b.z):
252                        if 2 <= len(same_rots[bone.name]):
253                            anm_data_raw[bone.name]["ROT"][same_rots[bone.name][-1].time] = same_rots[bone.name][-1].value.copy()
254                        anm_data_raw[bone.name]["ROT"][time] = rot.copy()
255                        same_rots[bone.name] = [KeyFrame(time, rot.copy(), a.copy())] # update last position and slope
256                    else:
257                        same_rots[bone.name].append(KeyFrame(time, rot.copy(), b.copy())) # update last position, but not last slope
258        
259        return anm_data_raw
260
261    def get_animation_keyframes(self, context, fps, pose, keyed_bones, fcurves):
262        anm_data_raw = {}
263
264        prop_sizes = {'location': 3, 'rotation_quaternion': 4, 'rotation_euler': 3}
265        
266        #class KeyFrame:
267        #    def __init__(self, time, value):
268        #        self.time = time
269        #        self.value = value
270        #same_locs = {}
271        #same_rots = {}
272        #pre_rots = {}
273        
274        def _convert_loc(pose_bone, loc):
275            loc = mathutils.Vector(loc)
276            loc = compat.mul(pose_bone.bone.matrix_local, loc)
277            if pose_bone.parent:
278                loc = compat.mul(pose_bone.parent.bone.matrix_local.inverted(), loc)
279                loc = compat.convert_bl_to_cm_bone_space(loc)
280            else:
281                loc = compat.convert_bl_to_cm_space(loc)
282            return loc * self.scale
283        """
284        def _convert_quat(pose_bone, quat):
285            #quat = mathutils.Quaternion(quat)
286            #'''Can't use matrix transforms here as they would mess up interpolation.'''
287            #quat = compat.mul(pose_bone.bone.matrix_local.to_quaternion(), quat)
288            
289            quat_mat = mathutils.Quaternion(quat).to_matrix().to_4x4()
290            quat_mat = compat.mul(pose_bone.bone.matrix_local, quat_mat)
291            #quat = quat_mat.to_quaternion()
292            if pose_bone.parent:
293                ## inverse of quat.w, quat.x, quat.y, quat.z = quat.w, -quat.z, quat.x, -quat.y
294                #quat.w, quat.x, quat.y, quat.z = quat.w, quat.y, -quat.z, -quat.x
295                #quat = compat.mul(pose_bone.parent.bone.matrix_local.to_quaternion().inverted(), quat)
296                ##quat = compat.mul(pose_bone.parent.bone.matrix_local.inverted().to_quaternion(), quat)\
297                quat_mat = compat.convert_bl_to_cm_bone_rotation(quat_mat)
298                quat_mat = compat.mul(pose_bone.parent.bone.matrix_local.inverted(), quat_mat)
299                quat_mat = compat.convert_bl_to_cm_bone_space(quat_mat)
300                quat = quat_mat.to_quaternion()
301            else:
302                #fix_quat = mathutils.Euler((0, 0, math.radians(-90)), 'XYZ').to_quaternion()
303                #fix_quat2 = mathutils.Euler((math.radians(-90), 0, 0), 'XYZ').to_quaternion()
304                #quat = compat.mul3(quat, fix_quat, fix_quat2)
305                #
306                #quat.w, quat.x, quat.y, quat.z = -quat.y, -quat.z, -quat.x, quat.w
307                
308                #quat.w, quat.x, quat.y, quat.z = quat.w, quat.y, -quat.z, -quat.x
309                #quat = compat.mul(mathutils.Matrix.Rotation(math.radians(90.0), 4, 'Z').to_quaternion(), quat)
310
311                quat_mat = compat.convert_bl_to_cm_bone_rotation(quat_mat)
312                quat_mat = compat.convert_bl_to_cm_space(quat_mat)
313                quat = quat_mat.to_quaternion()
314            return quat
315        """
316
317        def _convert_quat(pose_bone, quat):
318            bone_quat = pose_bone.bone.matrix.to_quaternion()
319            quat = mathutils.Quaternion(quat)
320
321            '''Can't use matrix transforms here as they would mess up interpolation.'''
322            quat = compat.mul(bone_quat, quat)
323            
324            if pose_bone.bone.parent:
325                #quat.w, quat.x, quat.y, quat.z = quat.w, -quat.z, quat.x, -quat.y
326                quat.w, quat.y, quat.x, quat.z = quat.w, -quat.z, quat.y, -quat.x
327            else:
328                quat = compat.mul(mathutils.Matrix.Rotation(math.radians(90.0), 4, 'Z').to_quaternion(), quat)
329                quat.w, quat.y, quat.x, quat.z = quat.w, -quat.z, quat.y, -quat.x
330            return quat
331
332        for prop, prop_keyed_bones in keyed_bones.items():
333            #self.report(type={'INFO'}, message=f_tip_("{prop} {list}", prop=prop, list=prop_keyed_bones))
334            for bone_name in prop_keyed_bones:
335                if bone_name not in anm_data_raw:
336                    anm_data_raw[bone_name] = {}
337                    #same_locs[bone_name] = []
338                    #same_rots[bone_name] = []
339                
340                pose_bone = pose.bones[bone_name]
341                rna_data_path = 'pose.bones["{bone_name}"].{property}'.format(bone_name=bone_name, property=prop)
342                prop_fcurves = [ fcurves.find(rna_data_path, index=axis_index) for axis_index in range(prop_sizes[prop]) ]
343                
344                # Create missing fcurves, and make existing fcurves CM3D2 compatible.
345                for axis_index, fcurve in enumerate(prop_fcurves):
346                    if not fcurve:
347                        fcurve = fcurves.new(rna_data_path, index=axis_index, action_group=pose_bone.name)
348                        prop_fcurves[axis_index] = fcurve
349                        self.report(type={'WARNING'}, message=f_tip_("Creating missing FCurve for {path}[{index}]", path=rna_data_path, index=axis_index))
350                    else:
351                        override = context.copy()
352                        override['active_editable_fcurve'] = fcurve
353                        bpy.ops.fcurve.convert_to_cm3d2_interpolation(override, only_selected=False, keep_reports=True)
354                        for kwargs in misc_DOPESHEET_MT_editor_menus.REPORTS:
355                            self.report(**kwargs)
356                        misc_DOPESHEET_MT_editor_menus.REPORTS.clear()
357
358
359                # Create a list by frame, indicating wether or not there is a keyframe at that time for each fcurve
360                is_keyframes = {}
361                for fcurve in prop_fcurves:
362                    for keyframe in fcurve.keyframe_points:
363                        frame = keyframe.co[0]
364                        if frame not in is_keyframes:
365                            is_keyframes[frame] = [False] * prop_sizes[prop]
366                        is_keyframes[frame][fcurve.array_index] = True
367                
368                # Make sure that no keyframe times are missing any components
369                for frame, is_axes in is_keyframes.items():
370                    for axis_index, is_axis in enumerate(is_axes):
371                        if not is_axis:
372                            fcurve = prop_fcurves[axis_index]
373                            keyframe = fcurve.keyframe_points.insert(
374                                frame         = frame                 , 
375                                value         = fcurve.evaluate(frame), 
376                                options       = {'NEEDED', 'FAST'}                        
377                            )
378                            self.report(type={'WARNING'}, message=f_tip_("Creating missing keyframe @ frame {frame} for {path}[{index}]", path=rna_data_path, index=axis_index, frame=frame))
379                
380                for fcurve in prop_fcurves:
381                    fcurve.update()
382                
383                for keyframe_index, frame in enumerate(is_keyframes.keys()):
384                    time = frame / fps * (1.0 / self.time_scale)
385
386                    _kf = lambda fcurve: fcurve.keyframe_points[keyframe_index]
387                    raw_keyframe = [ _kf(fc).co[1] for fc in prop_fcurves ]                                                                            
388                    tangent_in   = [ ( _kf(fc).handle_left [1] - _kf(fc).co[1] ) / ( _kf(fc).handle_left [0] - _kf(fc).co[0] ) * fps for fc in prop_fcurves ]
389                    tangent_out  = [ ( _kf(fc).handle_right[1] - _kf(fc).co[1] ) / ( _kf(fc).handle_right[0] - _kf(fc).co[0] ) * fps for fc in prop_fcurves ]
390                                                   
391                    if prop == 'location':
392                        if 'LOC' not in anm_data_raw[bone_name]:
393                            anm_data_raw[bone_name]['LOC'    ] = {}
394                            anm_data_raw[bone_name]['LOC_IN' ] = {}
395                            anm_data_raw[bone_name]['LOC_OUT'] = {}
396                        anm_data_raw[bone_name]['LOC'    ][time] = _convert_loc(pose_bone, raw_keyframe).copy()
397                        anm_data_raw[bone_name]['LOC_IN' ][time] = _convert_loc(pose_bone, tangent_in  ).copy()
398                        anm_data_raw[bone_name]['LOC_OUT'][time] = _convert_loc(pose_bone, tangent_out ).copy()
399                    elif prop == 'rotation_quaternion':
400                        if 'ROT' not in anm_data_raw[bone_name]:
401                            anm_data_raw[bone_name]['ROT'    ] = {}
402                            anm_data_raw[bone_name]['ROT_IN' ] = {}
403                            anm_data_raw[bone_name]['ROT_OUT'] = {}
404                        anm_data_raw[bone_name]['ROT'    ][time] = _convert_quat(pose_bone, raw_keyframe).copy()
405                        anm_data_raw[bone_name]['ROT_OUT'][time] = _convert_quat(pose_bone, tangent_out ).copy()
406                        anm_data_raw[bone_name]['ROT_IN' ][time] = _convert_quat(pose_bone, tangent_in  ).copy()
407                        # - - - Alternative Method - - -
408                        #raw_keyframe = mathutils.Quaternion(raw_keyframe)
409                        #tangent_in   = mathutils.Quaternion(tangent_in)
410                        #tangent_out  = mathutils.Quaternion(tangent_out)
411                        #converted_quat = _convert_quat(pose_bone, raw_keyframe).copy()
412                        #anm_data_raw[bone_name]['ROT'    ][time] = converted_quat.copy()
413                        #anm_data_raw[bone_name]['ROT_IN' ][time] = converted_quat.inverted() @ _convert_quat(pose_bone, raw_keyframe @ tangent_in  )
414                        #anm_data_raw[bone_name]['ROT_OUT'][time] = converted_quat.inverted() @ _convert_quat(pose_bone, raw_keyframe @ tangent_out )
415        
416        return anm_data_raw
417
418    def write_animation(self, context, file):
419        ob = context.active_object
420        arm = ob.data
421        pose = ob.pose
422        fps = context.scene.render.fps
423
424
425        bone_parents = {}
426        if self.bone_parent_from == 'ARMATURE_PROPERTY':
427            for i in range(9999):
428                name = "BoneData:" + str(i)
429                if name not in arm:
430                    continue
431                elems = arm[name].split(",")
432                if len(elems) != 5:
433                    continue
434                if elems[0] in arm.bones:
435                    if elems[2] in arm.bones:
436                        bone_parents[elems[0]] = arm.bones[elems[2]]
437                    else:
438                        bone_parents[elems[0]] = None
439            for bone in arm.bones:
440                if bone.name in bone_parents:
441                    continue
442                bone_parents[bone.name] = bone.parent
443        else:
444            for bone in arm.bones:
445                bone_parents[bone.name] = bone.parent
446
447        copied_action = None
448        if ob.animation_data and ob.animation_data.action:
449            copied_action = ob.animation_data.action.copy()
450            copied_action.name = ob.animation_data.action.name + "__anm_export"
451            fcurves = copied_action.fcurves
452            keyed_bones = {'location': [], 'rotation_quaternion': [], 'rotation_euler': []}
453            for bone in arm.bones:
454                rna_data_stub = 'pose.bones["{bone_name}"]'.format(bone_name=bone.name)
455                for prop, axes in [('location', 3), ('rotation_quaternion', 4), ('rotation_euler', 3)]:
456                    found_fcurve = False
457                    for axis_index in range(0, axes):
458                        if fcurves.find(rna_data_stub + '.' + prop, index=axis_index):
459                            found_fcurve = True
460                            break
461                    if found_fcurve:
462                        keyed_bones[prop].append(bone.name)
463
464        elif self.export_method == 'KEYED' or self.is_remove_unkeyed_bone:
465            raise common.CM3D2ExportException(
466                "Active armature has no animation data / action. Please use \"{method}\" with \"{option}\" disabled, or bake keyframes before exporting.".format(
467                    method = "Bake All Frames",
468                    option = "Remove Unkeyed Bones"
469                )
470            )
471
472
473
474
475        def is_japanese(string):
476            for ch in string:
477                name = unicodedata.name(ch)
478                if 'CJK UNIFIED' in name or 'HIRAGANA' in name or 'KATAKANA' in name:
479                    return True
480            return False
481        bones = []
482        already_bone_names = []
483        bones_queue = arm.bones[:]
484        while len(bones_queue):
485            bone = bones_queue.pop(0)
486
487            if not bone_parents[bone.name]:
488                already_bone_names.append(bone.name)
489                if self.is_remove_serial_number_bone:
490                    if common.has_serial_number(bone.name):
491                        continue
492                if self.is_remove_japanese_bone:
493                    if is_japanese(bone.name):
494                        continue
495                if self.is_remove_alone_bone and len(bone.children) == 0:
496                    continue
497                if self.is_remove_unkeyed_bone:
498                    is_keyed = False
499                    for prop in keyed_bones:
500                        if bone.name in keyed_bones[prop]:
501                            is_keyed = True
502                            break
503                    if not is_keyed:
504                        continue
505                bones.append(bone)
506                continue
507            elif bone_parents[bone.name].name in already_bone_names:
508                already_bone_names.append(bone.name)
509                if self.is_remove_serial_number_bone:
510                    if common.has_serial_number(bone.name):
511                        continue
512                if self.is_remove_japanese_bone:
513                    if is_japanese(bone.name):
514                        continue
515                if self.is_remove_ik_bone:
516                    bone_name_low = bone.name.lower()
517                    if '_ik_' in bone_name_low or bone_name_low.endswith('_nub') or bone.name.endswith('Nub'):
518                        continue
519                if self.is_remove_unkeyed_bone:
520                    is_keyed = False
521                    for prop in keyed_bones:
522                        if bone.name in keyed_bones[prop]:
523                            is_keyed = True
524                            break
525                    if not is_keyed:
526                        continue
527                bones.append(bone)
528                continue
529
530            bones_queue.append(bone)
531
532        if self.export_method == 'ALL':
533            anm_data_raw = self.get_animation_frames(context, fps, pose, bones, bone_parents)
534        elif self.export_method == 'KEYED':
535            anm_data_raw = self.get_animation_keyframes(context, fps, pose, keyed_bones, fcurves)
536
537        if copied_action:
538            context.blend_data.actions.remove(copied_action, do_unlink=True, do_id_user=True, do_ui_user=True)
539
540        anm_data = {}
541        for bone_name, channels in anm_data_raw.items():
542            anm_data[bone_name] = {100: {}, 101: {}, 102: {}, 103: {}, 104: {}, 105: {}, 106: {}}
543            if channels.get('LOC'):
544                has_tangents = bool(channels.get('LOC_IN') and channels.get('LOC_OUT'))
545                for time, loc in channels["LOC"].items():
546                    tangent_in  = channels['LOC_IN' ][time] if has_tangents else mathutils.Vector()
547                    tangent_out = channels['LOC_OUT'][time] if has_tangents else mathutils.Vector()
548                    anm_data[bone_name][104][time] = (loc.x, tangent_in.x, tangent_out.x)
549                    anm_data[bone_name][105][time] = (loc.y, tangent_in.y, tangent_out.y)
550                    anm_data[bone_name][106][time] = (loc.z, tangent_in.z, tangent_out.z)
551            if channels.get('ROT'):
552                has_tangents = bool(channels.get('ROT_IN') and channels.get('ROT_OUT'))
553                for time, rot in channels["ROT"].items():
554                    tangent_in  = channels['ROT_IN' ][time] if has_tangents else mathutils.Quaternion((0,0,0,0))
555                    tangent_out = channels['ROT_OUT'][time] if has_tangents else mathutils.Quaternion((0,0,0,0))
556                    anm_data[bone_name][100][time] = (rot.x, tangent_in.x, tangent_out.x)
557                    anm_data[bone_name][101][time] = (rot.y, tangent_in.y, tangent_out.y)
558                    anm_data[bone_name][102][time] = (rot.z, tangent_in.z, tangent_out.z)
559                    anm_data[bone_name][103][time] = (rot.w, tangent_in.w, tangent_out.w)
560                                                      
561        time_step = 1 / fps * (1.0 / self.time_scale)
562
563
564        ''' Write data to the file '''
565
566        common.write_str(file, 'CM3D2_ANIM')
567        file.write(struct.pack('<i', self.version))
568
569        for bone in bones:
570            if not anm_data.get(bone.name):
571                continue
572
573            file.write(struct.pack('<?', True))
574
575            bone_names = [bone.name]
576            current_bone = bone
577            while bone_parents[current_bone.name]:
578                bone_names.append(bone_parents[current_bone.name].name)
579                current_bone = bone_parents[current_bone.name]
580
581            bone_names.reverse()
582            common.write_str(file, "/".join(bone_names))
583            
584            for channel_id, keyframes in sorted(anm_data[bone.name].items(), key=lambda x: x[0]):
585                file.write(struct.pack('<B', channel_id))
586                file.write(struct.pack('<i', len(keyframes)))
587
588                keyframes_list = sorted(keyframes.items(), key=lambda x: x[0])
589                for i in range(len(keyframes_list)):
590                    x = keyframes_list[i][0]
591                    y, dydx_in, dydx_out = keyframes_list[i][1]
592
593                    if len(keyframes_list) <= 1:
594                        file.write(struct.pack('<f', x))
595                        file.write(struct.pack('<f', y))
596                        file.write(struct.pack('<2f', 0.0, 0.0))
597                        continue
598
599                    file.write(struct.pack('<f', x))
600                    file.write(struct.pack('<f', y))
601
602                    if self.is_smooth_handle and self.export_method == 'ALL':
603                        if i == 0:
604                            prev_x = x - (keyframes_list[i + 1][0] - x)
605                            prev_y = y - (keyframes_list[i + 1][1][0] - y)
606                            next_x = keyframes_list[i + 1][0]
607                            next_y = keyframes_list[i + 1][1][0]
608                        elif i == len(keyframes_list) - 1:
609                            prev_x = keyframes_list[i - 1][0]
610                            prev_y = keyframes_list[i - 1][1][0]
611                            next_x = x + (x - keyframes_list[i - 1][0])
612                            next_y = y + (y - keyframes_list[i - 1][1][0])
613                        else:
614                            prev_x = keyframes_list[i - 1][0]
615                            prev_y = keyframes_list[i - 1][1][0]
616                            next_x = keyframes_list[i + 1][0]
617                            next_y = keyframes_list[i + 1][1][0]
618
619                        prev_rad = (prev_y - y) / (prev_x - x)
620                        next_rad = (next_y - y) / (next_x - x)
621                        join_rad = (prev_rad + next_rad) / 2
622
623                        tan_in  = join_rad if x - prev_x <= time_step * 1.5 else prev_rad
624                        tan_out = join_rad if next_x - x <= time_step * 1.5 else next_rad
625                        
626                        file.write(struct.pack('<2f', tan_in, tan_out))
627                        #file.write(struct.pack('<2f', join_rad, join_rad))
628                        #file.write(struct.pack('<2f', prev_rad, next_rad))
629                    else:
630                        file.write(struct.pack('<2f', dydx_in, dydx_out))
631
632        file.write(struct.pack('<?', False))
633
634    def write_animation_from_text(self, context, file):
635        txt = context.blend_data.texts.get("AnmData")
636        if not txt:
637            raise common.CM3D2ExportException("There is no 'AnmData' text file.")
638
639        import json
640        anm_data = json.loads(txt.as_string())
641
642        common.write_str(file, 'CM3D2_ANIM')
643        file.write(struct.pack('<i', self.version))
644
645        for base_bone_name, bone_data in anm_data.items():
646            path = bone_data['path']
647            file.write(struct.pack('<?', True))
648            common.write_str(file, path)
649
650            for channel_id, channel in bone_data['channels'].items():
651                file.write(struct.pack('<B', int(channel_id)))
652                channel_data_count = len(channel)
653                file.write(struct.pack('<i', channel_data_count))
654                for channel_data in channel:
655                    frame = channel_data['frame']
656                    data = ( channel_data['f0'], channel_data['f1'], channel_data['f2'] )
657                    file.write(struct.pack('<f' , frame))
658                    file.write(struct.pack('<3f', *data ))
659
660        file.write(struct.pack('<?', False))
bl_idname = 'export_anim.export_cm3d2_anm'
bl_label = 'CM3D2モーション (.anm)'
bl_description = 'カスタムメイド3D2のanmファイルを保存します'
bl_options = {'REGISTER'}
filepath: <_PropertyDeferred, <built-in function StringProperty>, {'subtype': 'FILE_PATH', 'attr': 'filepath'}> = <_PropertyDeferred, <built-in function StringProperty>, {'subtype': 'FILE_PATH', 'attr': 'filepath'}>
filename_ext = '.anm'
filter_glob: <_PropertyDeferred, <built-in function StringProperty>, {'default': '*.anm', 'options': {'HIDDEN'}, 'attr': 'filter_glob'}> = <_PropertyDeferred, <built-in function StringProperty>, {'default': '*.anm', 'options': {'HIDDEN'}, 'attr': 'filter_glob'}>
scale: <_PropertyDeferred, <built-in function FloatProperty>, {'name': '倍率', 'default': 0.2, 'min': 0.1, 'max': 100, 'soft_min': 0.1, 'soft_max': 100, 'step': 100, 'precision': 1, 'description': 'エクスポート時のメッシュ等の拡大率です', 'attr': 'scale'}> = <_PropertyDeferred, <built-in function FloatProperty>, {'name': '倍率', 'default': 0.2, 'min': 0.1, 'max': 100, 'soft_min': 0.1, 'soft_max': 100, 'step': 100, 'precision': 1, 'description': 'エクスポート時のメッシュ等の拡大率です', 'attr': 'scale'}>
is_backup: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'ファイルをバックアップ', 'default': True, 'description': 'ファイルに上書きする場合にバックアップファイルを複製します', 'attr': 'is_backup'}> = <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'ファイルをバックアップ', 'default': True, 'description': 'ファイルに上書きする場合にバックアップファイルを複製します', 'attr': 'is_backup'}>
version: <_PropertyDeferred, <built-in function IntProperty>, {'name': 'ファイルバージョン', 'default': 1000, 'min': 1000, 'max': 1111, 'soft_min': 1000, 'soft_max': 1111, 'step': 1, 'attr': 'version'}> = <_PropertyDeferred, <built-in function IntProperty>, {'name': 'ファイルバージョン', 'default': 1000, 'min': 1000, 'max': 1111, 'soft_min': 1000, 'soft_max': 1111, 'step': 1, 'attr': 'version'}>
items = [('ARMATURE', 'アーマチュア', '', 'OUTLINER_OB_ARMATURE', 1), ('ARMATURE_PROPERTY', 'アーマチュア内プロパティ', '', 'ARMATURE_DATA', 2)]
export_method: <_PropertyDeferred, <built-in function EnumProperty>, {'items': [('ALL', 'Bake All Frames', 'Export every frame as a keyframe (legacy behavior, large file sizes)', 'SEQUENCE', 1), ('KEYED', 'Only Export Keyframes', 'Only export keyframes and their tangents (for more advance users)', 'KEYINGSET', 2), ('TEXT', 'From Anm Text JSON', "Export data from the JSON in the 'AnmData' text file", 'TEXT', 3)], 'name': 'Export Method', 'default': 'ALL', 'attr': 'export_method'}> = <_PropertyDeferred, <built-in function EnumProperty>, {'items': [('ALL', 'Bake All Frames', 'Export every frame as a keyframe (legacy behavior, large file sizes)', 'SEQUENCE', 1), ('KEYED', 'Only Export Keyframes', 'Only export keyframes and their tangents (for more advance users)', 'KEYINGSET', 2), ('TEXT', 'From Anm Text JSON', "Export data from the JSON in the 'AnmData' text file", 'TEXT', 3)], 'name': 'Export Method', 'default': 'ALL', 'attr': 'export_method'}>
frame_start: <_PropertyDeferred, <built-in function IntProperty>, {'name': '開始フレーム', 'default': 0, 'min': 0, 'max': 99999, 'soft_min': 0, 'soft_max': 99999, 'step': 1, 'attr': 'frame_start'}> = <_PropertyDeferred, <built-in function IntProperty>, {'name': '開始フレーム', 'default': 0, 'min': 0, 'max': 99999, 'soft_min': 0, 'soft_max': 99999, 'step': 1, 'attr': 'frame_start'}>
frame_end: <_PropertyDeferred, <built-in function IntProperty>, {'name': '最終フレーム', 'default': 0, 'min': 0, 'max': 99999, 'soft_min': 0, 'soft_max': 99999, 'step': 1, 'attr': 'frame_end'}> = <_PropertyDeferred, <built-in function IntProperty>, {'name': '最終フレーム', 'default': 0, 'min': 0, 'max': 99999, 'soft_min': 0, 'soft_max': 99999, 'step': 1, 'attr': 'frame_end'}>
key_frame_count: <_PropertyDeferred, <built-in function IntProperty>, {'name': 'キーフレーム数', 'default': 1, 'min': 1, 'max': 99999, 'soft_min': 1, 'soft_max': 99999, 'step': 1, 'attr': 'key_frame_count'}> = <_PropertyDeferred, <built-in function IntProperty>, {'name': 'キーフレーム数', 'default': 1, 'min': 1, 'max': 99999, 'soft_min': 1, 'soft_max': 99999, 'step': 1, 'attr': 'key_frame_count'}>
time_scale: <_PropertyDeferred, <built-in function FloatProperty>, {'name': '再生速度', 'default': 1.0, 'min': 0.1, 'max': 10.0, 'soft_min': 0.1, 'soft_max': 10.0, 'step': 10, 'precision': 1, 'attr': 'time_scale'}> = <_PropertyDeferred, <built-in function FloatProperty>, {'name': '再生速度', 'default': 1.0, 'min': 0.1, 'max': 10.0, 'soft_min': 0.1, 'soft_max': 10.0, 'step': 10, 'precision': 1, 'attr': 'time_scale'}>
is_keyframe_clean: <_PropertyDeferred, <built-in function BoolProperty>, {'name': '同じ変形のキーフレームを掃除', 'default': True, 'attr': 'is_keyframe_clean'}> = <_PropertyDeferred, <built-in function BoolProperty>, {'name': '同じ変形のキーフレームを掃除', 'default': True, 'attr': 'is_keyframe_clean'}>
is_visual_transform: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'Use Visual Transforms', 'default': True, 'attr': 'is_visual_transform'}> = <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'Use Visual Transforms', 'default': True, 'attr': 'is_visual_transform'}>
is_smooth_handle: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'キーフレーム間の変形をスムーズに', 'default': True, 'attr': 'is_smooth_handle'}> = <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'キーフレーム間の変形をスムーズに', 'default': True, 'attr': 'is_smooth_handle'}>
bone_parent_from: <_PropertyDeferred, <built-in function EnumProperty>, {'items': [('ARMATURE', 'アーマチュア', '', 'OUTLINER_OB_ARMATURE', 1), ('ARMATURE_PROPERTY', 'アーマチュア内プロパティ', '', 'ARMATURE_DATA', 2)], 'name': 'ボーン親情報の参照先', 'default': 'ARMATURE_PROPERTY', 'attr': 'bone_parent_from'}> = <_PropertyDeferred, <built-in function EnumProperty>, {'items': [('ARMATURE', 'アーマチュア', '', 'OUTLINER_OB_ARMATURE', 1), ('ARMATURE_PROPERTY', 'アーマチュア内プロパティ', '', 'ARMATURE_DATA', 2)], 'name': 'ボーン親情報の参照先', 'default': 'ARMATURE_PROPERTY', 'attr': 'bone_parent_from'}>
is_remove_unkeyed_bone: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'Remove Unkeyed Bones', 'default': False, 'attr': 'is_remove_unkeyed_bone'}> = <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'Remove Unkeyed Bones', 'default': False, 'attr': 'is_remove_unkeyed_bone'}>
is_remove_alone_bone: <_PropertyDeferred, <built-in function BoolProperty>, {'name': '親も子も存在しない', 'default': True, 'attr': 'is_remove_alone_bone'}> = <_PropertyDeferred, <built-in function BoolProperty>, {'name': '親も子も存在しない', 'default': True, 'attr': 'is_remove_alone_bone'}>
is_remove_ik_bone: <_PropertyDeferred, <built-in function BoolProperty>, {'name': '名前がIK/Nubっぽい', 'default': True, 'attr': 'is_remove_ik_bone'}> = <_PropertyDeferred, <built-in function BoolProperty>, {'name': '名前がIK/Nubっぽい', 'default': True, 'attr': 'is_remove_ik_bone'}>
is_remove_serial_number_bone: <_PropertyDeferred, <built-in function BoolProperty>, {'name': '名前が連番付き', 'default': True, 'attr': 'is_remove_serial_number_bone'}> = <_PropertyDeferred, <built-in function BoolProperty>, {'name': '名前が連番付き', 'default': True, 'attr': 'is_remove_serial_number_bone'}>
is_remove_japanese_bone: <_PropertyDeferred, <built-in function BoolProperty>, {'name': '名前に日本語が含まれる', 'default': True, 'attr': 'is_remove_japanese_bone'}> = <_PropertyDeferred, <built-in function BoolProperty>, {'name': '名前に日本語が含まれる', 'default': True, 'attr': 'is_remove_japanese_bone'}>
@classmethod
def poll(cls, context):
61    @classmethod
62    def poll(cls, context):
63        ob = context.active_object
64        if ob and ob.type == 'ARMATURE':
65            return True
66        return False
def invoke(self, context, event):
68    def invoke(self, context, event):
69        prefs = common.preferences()
70
71        ob = context.active_object
72        arm = ob.data
73        action_name = None
74        if ob.animation_data and ob.animation_data.action:
75            action_name = common.remove_serial_number(ob.animation_data.action.name)
76
77        if prefs.anm_default_path:
78            self.filepath = common.default_cm3d2_dir(prefs.anm_default_path, action_name, "anm")
79        else:
80            self.filepath = common.default_cm3d2_dir(prefs.anm_export_path, action_name, "anm")
81        self.frame_start = context.scene.frame_start
82        self.frame_end = context.scene.frame_end
83        self.scale = 1.0 / prefs.scale
84        self.is_backup = bool(prefs.backup_ext)
85        self.key_frame_count = (context.scene.frame_end - context.scene.frame_start) + 1
86
87        if "BoneData:0" in arm:
88            self.bone_parent_from = 'ARMATURE_PROPERTY'
89        else:
90            self.bone_parent_from = 'ARMATURE'
91
92        context.window_manager.fileselect_add(self)
93        return {'RUNNING_MODAL'}
def draw(self, context):
 95    def draw(self, context):
 96        self.layout.prop(self, 'scale')
 97
 98        box = self.layout.box()
 99        box.prop(self, 'is_backup', icon='FILE_BACKUP')
100        box.prop(self, 'version')
101
102        #self.layout.prop(self, 'is_anm_data_text', icon='TEXT')
103        box = self.layout.box()
104        box.label(text="Export Method")
105        box.prop(self, 'export_method', expand=True)
106
107        box = self.layout.box()
108        box.enabled = not (self.export_method == 'TEXT')
109        box.prop(self, 'time_scale')
110        sub_box = box.box()
111        sub_box.enabled = (self.export_method == 'ALL')
112        row = sub_box.row()
113        row.prop(self, 'frame_start')
114        row.prop(self, 'frame_end')
115        sub_box.prop(self, 'key_frame_count')
116        sub_box.prop(self, 'is_keyframe_clean', icon='DISCLOSURE_TRI_DOWN')
117        sub_box.prop(self, 'is_smooth_handle', icon='SMOOTHCURVE')
118
119        sub_box = box.box()
120        sub_box.label(text="ボーン親情報の参照先", icon='FILE_PARENT')
121        sub_box.prop(self, 'bone_parent_from', icon='FILE_PARENT', expand=True)
122
123        sub_box = box.box()
124        sub_box.label(text="除外するボーン", icon='X')
125        column = sub_box.column(align=True)
126        column.prop(self, 'is_remove_unkeyed_bone'      , icon='KEY_DEHLT'              )
127        column.prop(self, 'is_remove_alone_bone'        , icon='UNLINKED'               )
128        column.prop(self, 'is_remove_ik_bone'           , icon='CONSTRAINT_BONE'        )
129        column.prop(self, 'is_remove_serial_number_bone', icon='SEQUENCE'               )
130        column.prop(self, 'is_remove_japanese_bone'     , icon=compat.icon('HOLDOUT_ON'))
def execute(self, context):
132    def execute(self, context):
133        common.preferences().anm_export_path = self.filepath
134
135        try:
136            file = common.open_temporary(self.filepath, 'wb', is_backup=self.is_backup)
137        except:
138            self.report(type={'ERROR'}, message=f_tip_("ファイルを開くのに失敗しました、アクセス不可かファイルが存在しません。file={}", self.filepath))
139            return {'CANCELLED'}
140
141        try:
142            with file:
143                if self.export_method == 'TEXT':
144                    self.write_animation_from_text(context, file)
145                else:
146                    self.write_animation(context, file)
147        except common.CM3D2ExportException as e:
148            self.report(type={'ERROR'}, message=str(e))
149            return {'CANCELLED'}
150
151        return {'FINISHED'}
def get_animation_frames(self, context, fps, pose, bones, bone_parents):
153    def get_animation_frames(self, context, fps, pose, bones, bone_parents):
154        anm_data_raw = {}
155        class KeyFrame:
156            def __init__(self, time, value, slope=None):
157                self.time = time
158                self.value = value
159                if slope:
160                    self.slope = slope
161                elif type(value) == mathutils.Vector:
162                    self.slope = mathutils.Vector.Fill(len(value))
163                elif type(value) == mathutils.Quaternion:
164                    self.slope = mathutils.Quaternion((0,0,0,0))
165                else:
166                    self.slope = 0
167
168        same_locs = {}
169        same_rots = {}
170        pre_rots = {}
171        for key_frame_index in range(self.key_frame_count):
172            if self.key_frame_count == 1:
173                frame = 0.0
174            else:
175                frame = (self.frame_end - self.frame_start) / (self.key_frame_count - 1) * key_frame_index + self.frame_start
176            context.scene.frame_set(frame=int(frame), subframe=frame - int(frame))
177            if compat.IS_LEGACY:
178                context.scene.update()
179            else:
180                layer = context.view_layer
181                layer.update()
182
183            time = frame / fps * (1.0 / self.time_scale)
184
185            for bone in bones:
186                if bone.name not in anm_data_raw:
187                    anm_data_raw[bone.name] = {"LOC": {}, "ROT": {}}
188                    same_locs[bone.name] = []
189                    same_rots[bone.name] = []
190
191                pose_bone = pose.bones[bone.name]
192                pose_mat = pose_bone.matrix.copy() #ob.convert_space(pose_bone=pose_bone, matrix=pose_bone.matrix, from_space='POSE', to_space='WORLD')
193                parent = bone_parents[bone.name]
194                if parent:
195                    pose_mat = compat.convert_bl_to_cm_bone_rotation(pose_mat)
196                    pose_mat = compat.mul(pose.bones[parent.name].matrix.inverted(), pose_mat)
197                    pose_mat = compat.convert_bl_to_cm_bone_space(pose_mat)
198                else:
199                    pose_mat = compat.convert_bl_to_cm_bone_rotation(pose_mat)
200                    pose_mat = compat.convert_bl_to_cm_space(pose_mat)
201
202                loc = pose_mat.to_translation() * self.scale
203                rot = pose_mat.to_quaternion()
204
205                # This fixes rotations that jump to alternate representations.
206                if bone.name in pre_rots:
207                    if 5.0 < pre_rots[bone.name].rotation_difference(rot).angle:
208                        rot.w, rot.x, rot.y, rot.z = -rot.w, -rot.x, -rot.y, -rot.z
209                pre_rots[bone.name] = rot.copy()
210
211                #if parent:
212                #    #loc.x, loc.y, loc.z = -loc.y, -loc.x, loc.z
213                #    loc = compat.convert_bl_to_cm_bone_space(loc)
214                #    
215                #    # quat.w, quat.x, quat.y, quat.z = quat.w, -quat.z, quat.x, -quat.y
216                #    #rot.w, rot.x, rot.y, rot.z = rot.w, rot.y, rot.x, -rot.z
217                #    rot.w, rot.x, rot.y, rot.z = rot.w, rot.y, -rot.z, -rot.x
218                #
219                #else:
220                #    loc.x, loc.y, loc.z = -loc.x, loc.z, -loc.y
221                #
222                #    fix_quat = mathutils.Euler((0, 0, math.radians(-90)), 'XYZ').to_quaternion()
223                #    fix_quat2 = mathutils.Euler((math.radians(-90), 0, 0), 'XYZ').to_quaternion()
224                #    rot = compat.mul3(rot, fix_quat, fix_quat2)
225                #
226                #    rot.w, rot.x, rot.y, rot.z = -rot.y, -rot.z, -rot.x, rot.w
227                
228                if not self.is_keyframe_clean or key_frame_index == 0 or key_frame_index == self.key_frame_count - 1:
229                    anm_data_raw[bone.name]["LOC"][time] = loc.copy()
230                    anm_data_raw[bone.name]["ROT"][time] = rot.copy()
231
232                    if self.is_keyframe_clean:
233                        same_locs[bone.name].append(KeyFrame(time, loc.copy()))
234                        same_rots[bone.name].append(KeyFrame(time, rot.copy()))
235                else:
236                    def is_mismatch(a, b):
237                        return 0.000001 < abs(a - b)
238
239                    a = same_locs[bone.name][-1].value - loc
240                    b = same_locs[bone.name][-1].slope
241                    if is_mismatch(a.x, b.x) or is_mismatch(a.y, b.y) or is_mismatch(a.z, b.z):
242                        if 2 <= len(same_locs[bone.name]):
243                            anm_data_raw[bone.name]["LOC"][same_locs[bone.name][-1].time] = same_locs[bone.name][-1].value.copy()
244                        anm_data_raw[bone.name]["LOC"][time] = loc.copy()
245                        same_locs[bone.name] = [KeyFrame(time, loc.copy(), a.copy())] # update last position and slope
246                    else:
247                        same_locs[bone.name].append(KeyFrame(time, loc.copy(), b.copy())) # update last position, but not last slope
248                    
249                    a = same_rots[bone.name][-1].value - rot
250                    b = same_rots[bone.name][-1].slope
251                    if is_mismatch(a.w, b.w) or is_mismatch(a.x, b.x) or is_mismatch(a.y, b.y) or is_mismatch(a.z, b.z):
252                        if 2 <= len(same_rots[bone.name]):
253                            anm_data_raw[bone.name]["ROT"][same_rots[bone.name][-1].time] = same_rots[bone.name][-1].value.copy()
254                        anm_data_raw[bone.name]["ROT"][time] = rot.copy()
255                        same_rots[bone.name] = [KeyFrame(time, rot.copy(), a.copy())] # update last position and slope
256                    else:
257                        same_rots[bone.name].append(KeyFrame(time, rot.copy(), b.copy())) # update last position, but not last slope
258        
259        return anm_data_raw
def get_animation_keyframes(self, context, fps, pose, keyed_bones, fcurves):
261    def get_animation_keyframes(self, context, fps, pose, keyed_bones, fcurves):
262        anm_data_raw = {}
263
264        prop_sizes = {'location': 3, 'rotation_quaternion': 4, 'rotation_euler': 3}
265        
266        #class KeyFrame:
267        #    def __init__(self, time, value):
268        #        self.time = time
269        #        self.value = value
270        #same_locs = {}
271        #same_rots = {}
272        #pre_rots = {}
273        
274        def _convert_loc(pose_bone, loc):
275            loc = mathutils.Vector(loc)
276            loc = compat.mul(pose_bone.bone.matrix_local, loc)
277            if pose_bone.parent:
278                loc = compat.mul(pose_bone.parent.bone.matrix_local.inverted(), loc)
279                loc = compat.convert_bl_to_cm_bone_space(loc)
280            else:
281                loc = compat.convert_bl_to_cm_space(loc)
282            return loc * self.scale
283        """
284        def _convert_quat(pose_bone, quat):
285            #quat = mathutils.Quaternion(quat)
286            #'''Can't use matrix transforms here as they would mess up interpolation.'''
287            #quat = compat.mul(pose_bone.bone.matrix_local.to_quaternion(), quat)
288            
289            quat_mat = mathutils.Quaternion(quat).to_matrix().to_4x4()
290            quat_mat = compat.mul(pose_bone.bone.matrix_local, quat_mat)
291            #quat = quat_mat.to_quaternion()
292            if pose_bone.parent:
293                ## inverse of quat.w, quat.x, quat.y, quat.z = quat.w, -quat.z, quat.x, -quat.y
294                #quat.w, quat.x, quat.y, quat.z = quat.w, quat.y, -quat.z, -quat.x
295                #quat = compat.mul(pose_bone.parent.bone.matrix_local.to_quaternion().inverted(), quat)
296                ##quat = compat.mul(pose_bone.parent.bone.matrix_local.inverted().to_quaternion(), quat)\
297                quat_mat = compat.convert_bl_to_cm_bone_rotation(quat_mat)
298                quat_mat = compat.mul(pose_bone.parent.bone.matrix_local.inverted(), quat_mat)
299                quat_mat = compat.convert_bl_to_cm_bone_space(quat_mat)
300                quat = quat_mat.to_quaternion()
301            else:
302                #fix_quat = mathutils.Euler((0, 0, math.radians(-90)), 'XYZ').to_quaternion()
303                #fix_quat2 = mathutils.Euler((math.radians(-90), 0, 0), 'XYZ').to_quaternion()
304                #quat = compat.mul3(quat, fix_quat, fix_quat2)
305                #
306                #quat.w, quat.x, quat.y, quat.z = -quat.y, -quat.z, -quat.x, quat.w
307                
308                #quat.w, quat.x, quat.y, quat.z = quat.w, quat.y, -quat.z, -quat.x
309                #quat = compat.mul(mathutils.Matrix.Rotation(math.radians(90.0), 4, 'Z').to_quaternion(), quat)
310
311                quat_mat = compat.convert_bl_to_cm_bone_rotation(quat_mat)
312                quat_mat = compat.convert_bl_to_cm_space(quat_mat)
313                quat = quat_mat.to_quaternion()
314            return quat
315        """
316
317        def _convert_quat(pose_bone, quat):
318            bone_quat = pose_bone.bone.matrix.to_quaternion()
319            quat = mathutils.Quaternion(quat)
320
321            '''Can't use matrix transforms here as they would mess up interpolation.'''
322            quat = compat.mul(bone_quat, quat)
323            
324            if pose_bone.bone.parent:
325                #quat.w, quat.x, quat.y, quat.z = quat.w, -quat.z, quat.x, -quat.y
326                quat.w, quat.y, quat.x, quat.z = quat.w, -quat.z, quat.y, -quat.x
327            else:
328                quat = compat.mul(mathutils.Matrix.Rotation(math.radians(90.0), 4, 'Z').to_quaternion(), quat)
329                quat.w, quat.y, quat.x, quat.z = quat.w, -quat.z, quat.y, -quat.x
330            return quat
331
332        for prop, prop_keyed_bones in keyed_bones.items():
333            #self.report(type={'INFO'}, message=f_tip_("{prop} {list}", prop=prop, list=prop_keyed_bones))
334            for bone_name in prop_keyed_bones:
335                if bone_name not in anm_data_raw:
336                    anm_data_raw[bone_name] = {}
337                    #same_locs[bone_name] = []
338                    #same_rots[bone_name] = []
339                
340                pose_bone = pose.bones[bone_name]
341                rna_data_path = 'pose.bones["{bone_name}"].{property}'.format(bone_name=bone_name, property=prop)
342                prop_fcurves = [ fcurves.find(rna_data_path, index=axis_index) for axis_index in range(prop_sizes[prop]) ]
343                
344                # Create missing fcurves, and make existing fcurves CM3D2 compatible.
345                for axis_index, fcurve in enumerate(prop_fcurves):
346                    if not fcurve:
347                        fcurve = fcurves.new(rna_data_path, index=axis_index, action_group=pose_bone.name)
348                        prop_fcurves[axis_index] = fcurve
349                        self.report(type={'WARNING'}, message=f_tip_("Creating missing FCurve for {path}[{index}]", path=rna_data_path, index=axis_index))
350                    else:
351                        override = context.copy()
352                        override['active_editable_fcurve'] = fcurve
353                        bpy.ops.fcurve.convert_to_cm3d2_interpolation(override, only_selected=False, keep_reports=True)
354                        for kwargs in misc_DOPESHEET_MT_editor_menus.REPORTS:
355                            self.report(**kwargs)
356                        misc_DOPESHEET_MT_editor_menus.REPORTS.clear()
357
358
359                # Create a list by frame, indicating wether or not there is a keyframe at that time for each fcurve
360                is_keyframes = {}
361                for fcurve in prop_fcurves:
362                    for keyframe in fcurve.keyframe_points:
363                        frame = keyframe.co[0]
364                        if frame not in is_keyframes:
365                            is_keyframes[frame] = [False] * prop_sizes[prop]
366                        is_keyframes[frame][fcurve.array_index] = True
367                
368                # Make sure that no keyframe times are missing any components
369                for frame, is_axes in is_keyframes.items():
370                    for axis_index, is_axis in enumerate(is_axes):
371                        if not is_axis:
372                            fcurve = prop_fcurves[axis_index]
373                            keyframe = fcurve.keyframe_points.insert(
374                                frame         = frame                 , 
375                                value         = fcurve.evaluate(frame), 
376                                options       = {'NEEDED', 'FAST'}                        
377                            )
378                            self.report(type={'WARNING'}, message=f_tip_("Creating missing keyframe @ frame {frame} for {path}[{index}]", path=rna_data_path, index=axis_index, frame=frame))
379                
380                for fcurve in prop_fcurves:
381                    fcurve.update()
382                
383                for keyframe_index, frame in enumerate(is_keyframes.keys()):
384                    time = frame / fps * (1.0 / self.time_scale)
385
386                    _kf = lambda fcurve: fcurve.keyframe_points[keyframe_index]
387                    raw_keyframe = [ _kf(fc).co[1] for fc in prop_fcurves ]                                                                            
388                    tangent_in   = [ ( _kf(fc).handle_left [1] - _kf(fc).co[1] ) / ( _kf(fc).handle_left [0] - _kf(fc).co[0] ) * fps for fc in prop_fcurves ]
389                    tangent_out  = [ ( _kf(fc).handle_right[1] - _kf(fc).co[1] ) / ( _kf(fc).handle_right[0] - _kf(fc).co[0] ) * fps for fc in prop_fcurves ]
390                                                   
391                    if prop == 'location':
392                        if 'LOC' not in anm_data_raw[bone_name]:
393                            anm_data_raw[bone_name]['LOC'    ] = {}
394                            anm_data_raw[bone_name]['LOC_IN' ] = {}
395                            anm_data_raw[bone_name]['LOC_OUT'] = {}
396                        anm_data_raw[bone_name]['LOC'    ][time] = _convert_loc(pose_bone, raw_keyframe).copy()
397                        anm_data_raw[bone_name]['LOC_IN' ][time] = _convert_loc(pose_bone, tangent_in  ).copy()
398                        anm_data_raw[bone_name]['LOC_OUT'][time] = _convert_loc(pose_bone, tangent_out ).copy()
399                    elif prop == 'rotation_quaternion':
400                        if 'ROT' not in anm_data_raw[bone_name]:
401                            anm_data_raw[bone_name]['ROT'    ] = {}
402                            anm_data_raw[bone_name]['ROT_IN' ] = {}
403                            anm_data_raw[bone_name]['ROT_OUT'] = {}
404                        anm_data_raw[bone_name]['ROT'    ][time] = _convert_quat(pose_bone, raw_keyframe).copy()
405                        anm_data_raw[bone_name]['ROT_OUT'][time] = _convert_quat(pose_bone, tangent_out ).copy()
406                        anm_data_raw[bone_name]['ROT_IN' ][time] = _convert_quat(pose_bone, tangent_in  ).copy()
407                        # - - - Alternative Method - - -
408                        #raw_keyframe = mathutils.Quaternion(raw_keyframe)
409                        #tangent_in   = mathutils.Quaternion(tangent_in)
410                        #tangent_out  = mathutils.Quaternion(tangent_out)
411                        #converted_quat = _convert_quat(pose_bone, raw_keyframe).copy()
412                        #anm_data_raw[bone_name]['ROT'    ][time] = converted_quat.copy()
413                        #anm_data_raw[bone_name]['ROT_IN' ][time] = converted_quat.inverted() @ _convert_quat(pose_bone, raw_keyframe @ tangent_in  )
414                        #anm_data_raw[bone_name]['ROT_OUT'][time] = converted_quat.inverted() @ _convert_quat(pose_bone, raw_keyframe @ tangent_out )
415        
416        return anm_data_raw
def write_animation(self, context, file):
418    def write_animation(self, context, file):
419        ob = context.active_object
420        arm = ob.data
421        pose = ob.pose
422        fps = context.scene.render.fps
423
424
425        bone_parents = {}
426        if self.bone_parent_from == 'ARMATURE_PROPERTY':
427            for i in range(9999):
428                name = "BoneData:" + str(i)
429                if name not in arm:
430                    continue
431                elems = arm[name].split(",")
432                if len(elems) != 5:
433                    continue
434                if elems[0] in arm.bones:
435                    if elems[2] in arm.bones:
436                        bone_parents[elems[0]] = arm.bones[elems[2]]
437                    else:
438                        bone_parents[elems[0]] = None
439            for bone in arm.bones:
440                if bone.name in bone_parents:
441                    continue
442                bone_parents[bone.name] = bone.parent
443        else:
444            for bone in arm.bones:
445                bone_parents[bone.name] = bone.parent
446
447        copied_action = None
448        if ob.animation_data and ob.animation_data.action:
449            copied_action = ob.animation_data.action.copy()
450            copied_action.name = ob.animation_data.action.name + "__anm_export"
451            fcurves = copied_action.fcurves
452            keyed_bones = {'location': [], 'rotation_quaternion': [], 'rotation_euler': []}
453            for bone in arm.bones:
454                rna_data_stub = 'pose.bones["{bone_name}"]'.format(bone_name=bone.name)
455                for prop, axes in [('location', 3), ('rotation_quaternion', 4), ('rotation_euler', 3)]:
456                    found_fcurve = False
457                    for axis_index in range(0, axes):
458                        if fcurves.find(rna_data_stub + '.' + prop, index=axis_index):
459                            found_fcurve = True
460                            break
461                    if found_fcurve:
462                        keyed_bones[prop].append(bone.name)
463
464        elif self.export_method == 'KEYED' or self.is_remove_unkeyed_bone:
465            raise common.CM3D2ExportException(
466                "Active armature has no animation data / action. Please use \"{method}\" with \"{option}\" disabled, or bake keyframes before exporting.".format(
467                    method = "Bake All Frames",
468                    option = "Remove Unkeyed Bones"
469                )
470            )
471
472
473
474
475        def is_japanese(string):
476            for ch in string:
477                name = unicodedata.name(ch)
478                if 'CJK UNIFIED' in name or 'HIRAGANA' in name or 'KATAKANA' in name:
479                    return True
480            return False
481        bones = []
482        already_bone_names = []
483        bones_queue = arm.bones[:]
484        while len(bones_queue):
485            bone = bones_queue.pop(0)
486
487            if not bone_parents[bone.name]:
488                already_bone_names.append(bone.name)
489                if self.is_remove_serial_number_bone:
490                    if common.has_serial_number(bone.name):
491                        continue
492                if self.is_remove_japanese_bone:
493                    if is_japanese(bone.name):
494                        continue
495                if self.is_remove_alone_bone and len(bone.children) == 0:
496                    continue
497                if self.is_remove_unkeyed_bone:
498                    is_keyed = False
499                    for prop in keyed_bones:
500                        if bone.name in keyed_bones[prop]:
501                            is_keyed = True
502                            break
503                    if not is_keyed:
504                        continue
505                bones.append(bone)
506                continue
507            elif bone_parents[bone.name].name in already_bone_names:
508                already_bone_names.append(bone.name)
509                if self.is_remove_serial_number_bone:
510                    if common.has_serial_number(bone.name):
511                        continue
512                if self.is_remove_japanese_bone:
513                    if is_japanese(bone.name):
514                        continue
515                if self.is_remove_ik_bone:
516                    bone_name_low = bone.name.lower()
517                    if '_ik_' in bone_name_low or bone_name_low.endswith('_nub') or bone.name.endswith('Nub'):
518                        continue
519                if self.is_remove_unkeyed_bone:
520                    is_keyed = False
521                    for prop in keyed_bones:
522                        if bone.name in keyed_bones[prop]:
523                            is_keyed = True
524                            break
525                    if not is_keyed:
526                        continue
527                bones.append(bone)
528                continue
529
530            bones_queue.append(bone)
531
532        if self.export_method == 'ALL':
533            anm_data_raw = self.get_animation_frames(context, fps, pose, bones, bone_parents)
534        elif self.export_method == 'KEYED':
535            anm_data_raw = self.get_animation_keyframes(context, fps, pose, keyed_bones, fcurves)
536
537        if copied_action:
538            context.blend_data.actions.remove(copied_action, do_unlink=True, do_id_user=True, do_ui_user=True)
539
540        anm_data = {}
541        for bone_name, channels in anm_data_raw.items():
542            anm_data[bone_name] = {100: {}, 101: {}, 102: {}, 103: {}, 104: {}, 105: {}, 106: {}}
543            if channels.get('LOC'):
544                has_tangents = bool(channels.get('LOC_IN') and channels.get('LOC_OUT'))
545                for time, loc in channels["LOC"].items():
546                    tangent_in  = channels['LOC_IN' ][time] if has_tangents else mathutils.Vector()
547                    tangent_out = channels['LOC_OUT'][time] if has_tangents else mathutils.Vector()
548                    anm_data[bone_name][104][time] = (loc.x, tangent_in.x, tangent_out.x)
549                    anm_data[bone_name][105][time] = (loc.y, tangent_in.y, tangent_out.y)
550                    anm_data[bone_name][106][time] = (loc.z, tangent_in.z, tangent_out.z)
551            if channels.get('ROT'):
552                has_tangents = bool(channels.get('ROT_IN') and channels.get('ROT_OUT'))
553                for time, rot in channels["ROT"].items():
554                    tangent_in  = channels['ROT_IN' ][time] if has_tangents else mathutils.Quaternion((0,0,0,0))
555                    tangent_out = channels['ROT_OUT'][time] if has_tangents else mathutils.Quaternion((0,0,0,0))
556                    anm_data[bone_name][100][time] = (rot.x, tangent_in.x, tangent_out.x)
557                    anm_data[bone_name][101][time] = (rot.y, tangent_in.y, tangent_out.y)
558                    anm_data[bone_name][102][time] = (rot.z, tangent_in.z, tangent_out.z)
559                    anm_data[bone_name][103][time] = (rot.w, tangent_in.w, tangent_out.w)
560                                                      
561        time_step = 1 / fps * (1.0 / self.time_scale)
562
563
564        ''' Write data to the file '''
565
566        common.write_str(file, 'CM3D2_ANIM')
567        file.write(struct.pack('<i', self.version))
568
569        for bone in bones:
570            if not anm_data.get(bone.name):
571                continue
572
573            file.write(struct.pack('<?', True))
574
575            bone_names = [bone.name]
576            current_bone = bone
577            while bone_parents[current_bone.name]:
578                bone_names.append(bone_parents[current_bone.name].name)
579                current_bone = bone_parents[current_bone.name]
580
581            bone_names.reverse()
582            common.write_str(file, "/".join(bone_names))
583            
584            for channel_id, keyframes in sorted(anm_data[bone.name].items(), key=lambda x: x[0]):
585                file.write(struct.pack('<B', channel_id))
586                file.write(struct.pack('<i', len(keyframes)))
587
588                keyframes_list = sorted(keyframes.items(), key=lambda x: x[0])
589                for i in range(len(keyframes_list)):
590                    x = keyframes_list[i][0]
591                    y, dydx_in, dydx_out = keyframes_list[i][1]
592
593                    if len(keyframes_list) <= 1:
594                        file.write(struct.pack('<f', x))
595                        file.write(struct.pack('<f', y))
596                        file.write(struct.pack('<2f', 0.0, 0.0))
597                        continue
598
599                    file.write(struct.pack('<f', x))
600                    file.write(struct.pack('<f', y))
601
602                    if self.is_smooth_handle and self.export_method == 'ALL':
603                        if i == 0:
604                            prev_x = x - (keyframes_list[i + 1][0] - x)
605                            prev_y = y - (keyframes_list[i + 1][1][0] - y)
606                            next_x = keyframes_list[i + 1][0]
607                            next_y = keyframes_list[i + 1][1][0]
608                        elif i == len(keyframes_list) - 1:
609                            prev_x = keyframes_list[i - 1][0]
610                            prev_y = keyframes_list[i - 1][1][0]
611                            next_x = x + (x - keyframes_list[i - 1][0])
612                            next_y = y + (y - keyframes_list[i - 1][1][0])
613                        else:
614                            prev_x = keyframes_list[i - 1][0]
615                            prev_y = keyframes_list[i - 1][1][0]
616                            next_x = keyframes_list[i + 1][0]
617                            next_y = keyframes_list[i + 1][1][0]
618
619                        prev_rad = (prev_y - y) / (prev_x - x)
620                        next_rad = (next_y - y) / (next_x - x)
621                        join_rad = (prev_rad + next_rad) / 2
622
623                        tan_in  = join_rad if x - prev_x <= time_step * 1.5 else prev_rad
624                        tan_out = join_rad if next_x - x <= time_step * 1.5 else next_rad
625                        
626                        file.write(struct.pack('<2f', tan_in, tan_out))
627                        #file.write(struct.pack('<2f', join_rad, join_rad))
628                        #file.write(struct.pack('<2f', prev_rad, next_rad))
629                    else:
630                        file.write(struct.pack('<2f', dydx_in, dydx_out))
631
632        file.write(struct.pack('<?', False))
def write_animation_from_text(self, context, file):
634    def write_animation_from_text(self, context, file):
635        txt = context.blend_data.texts.get("AnmData")
636        if not txt:
637            raise common.CM3D2ExportException("There is no 'AnmData' text file.")
638
639        import json
640        anm_data = json.loads(txt.as_string())
641
642        common.write_str(file, 'CM3D2_ANIM')
643        file.write(struct.pack('<i', self.version))
644
645        for base_bone_name, bone_data in anm_data.items():
646            path = bone_data['path']
647            file.write(struct.pack('<?', True))
648            common.write_str(file, path)
649
650            for channel_id, channel in bone_data['channels'].items():
651                file.write(struct.pack('<B', int(channel_id)))
652                channel_data_count = len(channel)
653                file.write(struct.pack('<i', channel_data_count))
654                for channel_data in channel:
655                    frame = channel_data['frame']
656                    data = ( channel_data['f0'], channel_data['f1'], channel_data['f2'] )
657                    file.write(struct.pack('<f' , frame))
658                    file.write(struct.pack('<3f', *data ))
659
660        file.write(struct.pack('<?', False))
bl_rna = <bpy_struct, Struct("EXPORT_ANIM_OT_export_cm3d2_anm")>
Inherited Members
bpy_types.Operator
as_keywords
poll_message_set
builtins.bpy_struct
keys
values
get
pop
as_pointer
keyframe_insert
keyframe_delete
driver_add
driver_remove
is_property_set
property_unset
is_property_hidden
is_property_readonly
is_property_overridable_library
property_overridable_library_set
path_resolve
path_from_id
type_recast
bl_rna_get_subclass_py
bl_rna_get_subclass
id_properties_ensure
id_properties_clear
id_properties_ui
id_data